Skip to content

Commit

Permalink
Add knot.toml schema
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser committed Jan 24, 2025
1 parent 9353482 commit b331d2f
Show file tree
Hide file tree
Showing 16 changed files with 1,008 additions and 92 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/red_knot_project/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pep440_rs = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
Expand All @@ -40,8 +41,9 @@ insta = { workspace = true, features = ["redactions", "ron"] }

[features]
default = ["zstd"]
zstd = ["red_knot_vendored/zstd"]
deflate = ["red_knot_vendored/deflate"]
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
zstd = ["red_knot_vendored/zstd"]

[lints]
workspace = true
58 changes: 58 additions & 0 deletions crates/red_knot_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ use thiserror::Error;
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
/// Configures the type checking environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<EnvironmentOptions>,

#[serde(skip_serializing_if = "Option::is_none")]
pub src: Option<SrcOptions>,

/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
}
Expand Down Expand Up @@ -177,6 +180,7 @@ impl Options {

#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EnvironmentOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub python_version: Option<RangedValue<PythonVersion>>,
Expand Down Expand Up @@ -204,6 +208,7 @@ pub struct EnvironmentOptions {

#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SrcOptions {
/// The root of the project, used for finding first-party modules.
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -212,7 +217,9 @@ pub struct SrcOptions {

#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rules {
#[schemars(schema_with = "schema::rules")]
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
}

Expand All @@ -226,6 +233,57 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
}
}

#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::Level;
use schemars::gen::SchemaGenerator;
use schemars::schema::{
InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
};

pub(super) fn rules(gen: &mut SchemaGenerator) -> Schema {
let registry = &*DEFAULT_LINT_REGISTRY;

let level_schema = gen.subschema_for::<Level>();

let properties: schemars::Map<String, Schema> = registry
.lints()
.iter()
.map(|lint| {
(
lint.name().to_string(),
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![level_schema.clone()]),
..Default::default()
})),
metadata: Some(Box::new(Metadata {
title: Some(lint.summary().to_string()),
description: Some(lint.documentation()),
deprecated: lint.status.is_deprecated(),
default: Some(lint.default_level.to_string().into()),
..Metadata::default()
})),
..Default::default()
}),
)
})
.collect();

Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties,
additional_properties: Some(Box::new(Schema::Bool(false))),
..ObjectValidation::default()
})),

..Default::default()
})
}
}

#[derive(Error, Debug)]
pub enum KnotTomlError {
#[error(transparent)]
Expand Down
43 changes: 18 additions & 25 deletions crates/red_knot_project/src/metadata/value.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::combine::Combine;
use crate::Db;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_macros::Combine;
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Deserializer};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::fmt;
Expand Down Expand Up @@ -70,15 +71,19 @@ impl Drop for ValueSourceGuard {
///
/// This ensures that two resolved configurations are identical even if the position of a value has changed
/// or if the values were loaded from different sources.
#[derive(Clone)]
#[derive(Clone, serde::Serialize)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RangedValue<T> {
value: T,
#[serde(skip)]
source: ValueSource,

/// The byte range of `value` in `source`.
///
/// Can be `None` because not all sources support a range.
/// For example, arguments provided on the CLI won't have a range attached.
#[serde(skip)]
range: Option<TextRange>,
}

Expand Down Expand Up @@ -266,18 +271,6 @@ where
}
}

impl<T> Serialize for RangedValue<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.value.serialize(serializer)
}
}

/// A possibly relative path in a configuration file.
///
/// Relative paths in configuration files or from CLI options
Expand All @@ -286,9 +279,19 @@ where
/// * CLI: The path is relative to the current working directory
/// * Configuration file: The path is relative to the project's root.
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Combine,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);

impl RelativePathBuf {
Expand Down Expand Up @@ -325,13 +328,3 @@ impl RelativePathBuf {
SystemPath::absolute(&self.0, relative_to)
}
}

impl Combine for RelativePathBuf {
fn combine(self, other: Self) -> Self {
Self(self.0.combine(other.0))
}

fn combine_with(&mut self, other: Self) {
self.0.combine_with(other.0);
}
}
1 change: 1 addition & 0 deletions crates/red_knot_python_semantic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
hashbrown = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
Expand Down
27 changes: 23 additions & 4 deletions crates/red_knot_python_semantic/src/lint.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use core::fmt;
use itertools::Itertools;
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use rustc_hash::FxHashMap;
use std::fmt::Formatter;
use std::hash::Hasher;
use thiserror::Error;

Expand Down Expand Up @@ -36,6 +38,7 @@ pub struct LintMetadata {
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Level {
/// The lint is disabled and should not run.
Ignore,
Expand All @@ -61,6 +64,16 @@ impl Level {
}
}

impl fmt::Display for Level {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Level::Ignore => f.write_str("ignore"),
Level::Warn => f.write_str("warn"),
Level::Error => f.write_str("error"),
}
}
}

impl TryFrom<Level> for Severity {
type Error = ();

Expand All @@ -84,9 +97,11 @@ impl LintMetadata {

/// Returns the documentation line by line with one leading space and all trailing whitespace removed.
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
self.raw_documentation
.lines()
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
self.raw_documentation.lines().map(|line| {
line.strip_prefix(char::is_whitespace)
.unwrap_or(line)
.trim_end()
})
}

/// Returns the documentation as a single string.
Expand Down Expand Up @@ -180,6 +195,10 @@ impl LintStatus {
pub const fn is_removed(&self) -> bool {
matches!(self, LintStatus::Removed { .. })
}

pub const fn is_deprecated(&self) -> bool {
matches!(self, LintStatus::Deprecated { .. })
}
}

/// Declares a lint rule with the given metadata.
Expand Down Expand Up @@ -223,7 +242,7 @@ macro_rules! declare_lint {
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
summary: $summary,
raw_documentation: concat!($($doc,)+ "\n"),
raw_documentation: concat!($($doc, '\n',)+),
status: $status,
file: file!(),
line: line!(),
Expand Down
44 changes: 44 additions & 0 deletions crates/red_knot_python_semantic/src/python_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum PythonPlatform {
/// Do not make any assumptions about the target platform.
#[default]
All,

/// Assume a specific target platform like `linux`, `darwin` or `win32`.
///
/// We use a string (instead of individual enum variants), as the set of possible platforms
Expand All @@ -28,3 +29,46 @@ impl Display for PythonPlatform {
}
}
}

#[cfg(feature = "schemars")]
mod schema {
use crate::PythonPlatform;
use schemars::gen::SchemaGenerator;
use schemars::schema::{Schema, SchemaObject, SubschemaValidation};
use schemars::JsonSchema;

impl JsonSchema for PythonPlatform {
fn schema_name() -> String {
"PythonPlatform".to_string()
}

fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),

// Hard code some well known values, but allow any other string as well.
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
enum_values: Some(vec![
"all".to_string().into(),
"linux".to_string().into(),
"darwin".to_string().into(),
"win32".to_string().into(),
]),
..SchemaObject::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..SchemaObject::default()
}),
]),
..SubschemaValidation::default()
})),

..SchemaObject::default()
})
}
}
}
Loading

0 comments on commit b331d2f

Please sign in to comment.