mirror of
https://github.com/astral-sh/ruff
synced 2026-01-20 13:00:52 -05:00
[ty] Support overriding respect-type-ignore-comments (#22615)
This commit is contained in:
38
crates/ty/docs/configuration.md
generated
38
crates/ty/docs/configuration.md
generated
@@ -444,6 +444,44 @@ severity levels or disable them entirely.
|
||||
|
||||
---
|
||||
|
||||
## `overrides.analysis`
|
||||
|
||||
#### `respect-type-ignore-comments`
|
||||
|
||||
Whether ty should respect `type: ignore` comments.
|
||||
|
||||
When set to `false`, `type: ignore` comments are treated like any other normal
|
||||
comment and can't be used to suppress ty errors (you have to use `ty: ignore` instead).
|
||||
|
||||
Setting this option can be useful when using ty alongside other type checkers or when
|
||||
you prefer using `ty: ignore` over `type: ignore`.
|
||||
|
||||
Defaults to `true`.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.ty.overrides.analysis]
|
||||
# Disable support for `type: ignore` comments
|
||||
respect-type-ignore-comments = false
|
||||
```
|
||||
|
||||
=== "ty.toml"
|
||||
|
||||
```toml
|
||||
[overrides.analysis]
|
||||
# Disable support for `type: ignore` comments
|
||||
respect-type-ignore-comments = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `src`
|
||||
|
||||
### `exclude`
|
||||
|
||||
@@ -41,3 +41,172 @@ fn respect_type_ignore_comments_is_turned_off() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Basic override functionality: override analysis options for a specific file
|
||||
#[test]
|
||||
fn overrides_basic() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.analysis]
|
||||
respect-type-ignore-comments = true
|
||||
|
||||
[[tool.ty.overrides]]
|
||||
include = ["tests/**"]
|
||||
|
||||
[tool.ty.overrides.analysis]
|
||||
respect-type-ignore-comments = false
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
print(x) # type: ignore # ignore respected (global)
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"tests/test_main.py",
|
||||
r#"
|
||||
print(x) # type: ignore # ignore not-respected (override)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `x` used when not defined
|
||||
--> tests/test_main.py:2:7
|
||||
|
|
||||
2 | print(x) # type: ignore # ignore not-respected (override)
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Multiple overrides: later overrides take precedence
|
||||
#[test]
|
||||
fn overrides_precedence() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.analysis]
|
||||
respect-type-ignore-comments = true
|
||||
|
||||
# First override: all test files
|
||||
[[tool.ty.overrides]]
|
||||
include = ["tests/**"]
|
||||
[tool.ty.overrides.analysis]
|
||||
respect-type-ignore-comments = false
|
||||
|
||||
# Second override: specific test file (takes precedence)
|
||||
[[tool.ty.overrides]]
|
||||
include = ["tests/important.py"]
|
||||
[tool.ty.overrides.analysis]
|
||||
respect-type-ignore-comments = true
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"tests/test_main.py",
|
||||
r#"
|
||||
print(y) # type: ignore (should be an error, because type ignores are disabled)
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"tests/important.py",
|
||||
r#"
|
||||
print(y) # type: ignore (no error, because type ignores are enabled)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `y` used when not defined
|
||||
--> tests/test_main.py:2:7
|
||||
|
|
||||
2 | print(y) # type: ignore (should be an error, because type ignores are disabled)
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Override without analysis options inherit the global analysis options
|
||||
#[test]
|
||||
fn overrides_inherit_global() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"pyproject.toml",
|
||||
r#"
|
||||
[tool.ty.analysis]
|
||||
respect-type-ignore-comments = false
|
||||
|
||||
[[tool.ty.overrides]]
|
||||
include = ["tests/**"]
|
||||
|
||||
[tool.ty.overrides.rules]
|
||||
division-by-zero = "warn"
|
||||
|
||||
[tool.ty.overrides.analysis]
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"main.py",
|
||||
r#"
|
||||
print(y) # type: ignore ignore not-respected (global)
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"tests/test_main.py",
|
||||
r#"
|
||||
print(y) # type: ignore ignore respected (inherited from global)
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `y` used when not defined
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | print(y) # type: ignore ignore not-respected (global)
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `y` used when not defined
|
||||
--> tests/test_main.py:2:7
|
||||
|
|
||||
2 | print(y) # type: ignore ignore respected (inherited from global)
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -796,12 +796,12 @@ fn overrides_no_actual_overrides() -> anyhow::Result<()> {
|
||||
3 | division-by-zero = "error"
|
||||
4 |
|
||||
5 | [[tool.ty.overrides]]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ This overrides section configures no rules
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ This overrides section overrides no settings
|
||||
6 | include = ["*.py"] # Has patterns but no rule overrides
|
||||
7 | # Missing [tool.ty.overrides.rules] section entirely
|
||||
|
|
||||
info: It has no `rules` table
|
||||
info: Add a `[overrides.rules]` table...
|
||||
info: It has no `rules` or `analysis` table
|
||||
info: Add a `[overrides.rules]` or `[overrides.analysis]` table...
|
||||
info: or remove the `[[overrides]]` section if there's nothing to override
|
||||
|
||||
error[division-by-zero]: Cannot divide object of type `Literal[4]` by zero
|
||||
|
||||
@@ -470,8 +470,9 @@ impl SemanticDb for ProjectDatabase {
|
||||
ty_python_semantic::default_lint_registry()
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
self.project().settings(self).analysis()
|
||||
fn analysis_settings(&self, file: File) -> &AnalysisSettings {
|
||||
let settings = file_settings(self, file);
|
||||
settings.analysis(self)
|
||||
}
|
||||
|
||||
fn verbose(&self) -> bool {
|
||||
@@ -661,7 +662,7 @@ pub(crate) mod tests {
|
||||
ty_python_semantic::default_lint_registry()
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
fn analysis_settings(&self, _file: ruff_db::files::File) -> &AnalysisSettings {
|
||||
self.project().settings(self).analysis()
|
||||
}
|
||||
|
||||
|
||||
@@ -480,8 +480,13 @@ impl Options {
|
||||
let mut overrides = Vec::with_capacity(override_options.len());
|
||||
|
||||
for override_option in override_options {
|
||||
let override_instance =
|
||||
override_option.to_override(db, project_root, self.rules.as_ref(), diagnostics)?;
|
||||
let override_instance = override_option.to_override(
|
||||
db,
|
||||
project_root,
|
||||
self.rules.as_ref(),
|
||||
self.analysis.as_ref(),
|
||||
diagnostics,
|
||||
)?;
|
||||
|
||||
if let Some(value) = override_instance {
|
||||
overrides.push(value);
|
||||
@@ -1266,6 +1271,7 @@ pub struct TerminalOptions {
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Combine,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
@@ -1296,7 +1302,7 @@ pub struct AnalysisOptions {
|
||||
}
|
||||
|
||||
impl AnalysisOptions {
|
||||
fn to_settings(&self) -> AnalysisSettings {
|
||||
pub(super) fn to_settings(&self) -> AnalysisSettings {
|
||||
let AnalysisSettings {
|
||||
respect_type_ignore_comments: respect_type_ignore_default,
|
||||
} = AnalysisSettings::default();
|
||||
@@ -1446,6 +1452,10 @@ pub struct OverrideOptions {
|
||||
"#
|
||||
)]
|
||||
pub rules: Option<Rules>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[option_group]
|
||||
pub analysis: Option<AnalysisOptions>,
|
||||
}
|
||||
|
||||
impl RangedValue<OverrideOptions> {
|
||||
@@ -1454,36 +1464,49 @@ impl RangedValue<OverrideOptions> {
|
||||
db: &dyn Db,
|
||||
project_root: &SystemPath,
|
||||
global_rules: Option<&Rules>,
|
||||
global_analysis: Option<&AnalysisOptions>,
|
||||
diagnostics: &mut Vec<OptionDiagnostic>,
|
||||
) -> Result<Option<Override>, Box<OptionDiagnostic>> {
|
||||
let rules = self.rules.or_default();
|
||||
let analysis = self.analysis.or_default();
|
||||
|
||||
// First, warn about incorrect or useless overrides.
|
||||
if rules.is_empty() {
|
||||
if rules.is_empty() && *analysis == AnalysisOptions::default() {
|
||||
let mut diagnostic = OptionDiagnostic::new(
|
||||
DiagnosticId::UselessOverridesSection,
|
||||
"Useless `overrides` section".to_string(),
|
||||
Severity::Warning,
|
||||
);
|
||||
|
||||
diagnostic = if self.rules.is_none() {
|
||||
diagnostic = if self.rules.is_none() && self.analysis.is_none() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"It has no `rules` table",
|
||||
"It has no `rules` or `analysis` table",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"Add a `[overrides.rules]` table...",
|
||||
"Add a `[overrides.rules]` or `[overrides.analysis]` table...",
|
||||
))
|
||||
} else {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"The rules table is empty",
|
||||
));
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"Add a rule to `[overrides.rules]` to override specific rules...",
|
||||
))
|
||||
if self.rules.is_some() && rules.is_empty() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"The `rules` table is empty",
|
||||
));
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"Add a rule to `[overrides.rules]` to override specific rules...",
|
||||
));
|
||||
}
|
||||
|
||||
if self.analysis.is_some() && *analysis == AnalysisOptions::default() {
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
SubDiagnosticSeverity::Info,
|
||||
"The `analysis` table is empty",
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic
|
||||
};
|
||||
|
||||
diagnostic = diagnostic.sub(SubDiagnostic::new(
|
||||
@@ -1496,7 +1519,7 @@ impl RangedValue<OverrideOptions> {
|
||||
if let Ok(file) = system_path_to_file(db, source_file) {
|
||||
let annotation =
|
||||
Annotation::primary(Span::from(file).with_optional_range(self.range()))
|
||||
.message("This overrides section configures no rules");
|
||||
.message("This overrides section overrides no settings");
|
||||
diagnostic = diagnostic.with_annotation(Some(annotation));
|
||||
}
|
||||
}
|
||||
@@ -1585,13 +1608,23 @@ impl RangedValue<OverrideOptions> {
|
||||
// Convert merged rules to rule selection
|
||||
let rule_selection = merged_rules.to_rule_selection(db, diagnostics);
|
||||
|
||||
let mut merged_analysis = analysis.into_owned();
|
||||
|
||||
if let Some(global_analysis) = global_analysis {
|
||||
merged_analysis = merged_analysis.combine(global_analysis.clone());
|
||||
}
|
||||
|
||||
let analysis = merged_analysis.to_settings();
|
||||
|
||||
let override_instance = Override {
|
||||
files,
|
||||
options: Arc::new(InnerOverrideOptions {
|
||||
rules: self.rules.clone(),
|
||||
analysis: self.analysis.clone(),
|
||||
}),
|
||||
settings: Arc::new(OverrideSettings {
|
||||
rules: rule_selection,
|
||||
analysis,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1605,6 +1638,8 @@ pub(super) struct InnerOverrideOptions {
|
||||
/// Raw rule options as specified in the configuration.
|
||||
/// Used when multiple overrides match a file and need to be merged.
|
||||
pub(super) rules: Option<Rules>,
|
||||
|
||||
pub(super) analysis: Option<AnalysisOptions>,
|
||||
}
|
||||
|
||||
/// Error returned when the settings can't be resolved because of a hard error.
|
||||
|
||||
@@ -171,18 +171,26 @@ fn merge_overrides(db: &dyn Db, overrides: Vec<Arc<InnerOverrideOptions>>, _: ()
|
||||
merged.combine_with((*option).clone());
|
||||
}
|
||||
|
||||
merged
|
||||
.rules
|
||||
.combine_with(db.project().metadata(db).options().rules.clone());
|
||||
let global_options = db.project().metadata(db).options();
|
||||
|
||||
let Some(rules) = merged.rules else {
|
||||
merged.rules.combine_with(global_options.rules.clone());
|
||||
merged
|
||||
.analysis
|
||||
.combine_with(global_options.analysis.clone());
|
||||
|
||||
if merged.rules.is_none() && merged.analysis.is_none() {
|
||||
return FileSettings::Global;
|
||||
};
|
||||
}
|
||||
|
||||
let rules = merged.rules.unwrap_or_default();
|
||||
let analysis = merged.analysis.unwrap_or_default();
|
||||
|
||||
// It's okay to ignore the errors here because the rules are eagerly validated
|
||||
// during `overrides.to_settings()`.
|
||||
let rules = rules.to_rule_selection(db, &mut Vec::new());
|
||||
FileSettings::File(Arc::new(OverrideSettings { rules }))
|
||||
let analysis = analysis.to_settings();
|
||||
|
||||
FileSettings::File(Arc::new(OverrideSettings { rules, analysis }))
|
||||
}
|
||||
|
||||
/// The resolved settings for a file.
|
||||
@@ -202,9 +210,17 @@ impl FileSettings {
|
||||
FileSettings::File(override_settings) => &override_settings.rules,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analysis<'a>(&'a self, db: &'a dyn Db) -> &'a AnalysisSettings {
|
||||
match self {
|
||||
FileSettings::Global => db.project().settings(db).analysis(),
|
||||
FileSettings::File(override_settings) => &override_settings.analysis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, get_size2::GetSize)]
|
||||
pub struct OverrideSettings {
|
||||
pub(super) rules: RuleSelection,
|
||||
pub(super) analysis: AnalysisSettings,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub trait Db: ModuleResolverDb {
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry;
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings;
|
||||
fn analysis_settings(&self, file: File) -> &AnalysisSettings;
|
||||
|
||||
/// Whether ty is running with logging verbosity INFO or higher (`-v` or more).
|
||||
fn verbose(&self) -> bool;
|
||||
@@ -138,7 +138,7 @@ pub(crate) mod tests {
|
||||
default_lint_registry()
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
fn analysis_settings(&self, _file: File) -> &AnalysisSettings {
|
||||
&self.analysis_settings
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let source = source_text(db, file);
|
||||
|
||||
let respect_type_ignore = db.analysis_settings().respect_type_ignore_comments;
|
||||
let respect_type_ignore = db.analysis_settings(file).respect_type_ignore_comments;
|
||||
|
||||
let mut builder = SuppressionsBuilder::new(&source, db.lint_registry());
|
||||
let mut line_start = TextSize::default();
|
||||
|
||||
@@ -268,7 +268,7 @@ impl ty_python_semantic::Db for CorpusDb {
|
||||
false
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
fn analysis_settings(&self, _file: File) -> &AnalysisSettings {
|
||||
&self.analysis_settings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ impl SemanticDb for Db {
|
||||
false
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
fn analysis_settings(&self, _file: File) -> &AnalysisSettings {
|
||||
self.settings().analysis(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ impl SemanticDb for TestDb {
|
||||
&self.rule_selection
|
||||
}
|
||||
|
||||
fn analysis_settings(&self) -> &AnalysisSettings {
|
||||
fn analysis_settings(&self, _file: File) -> &AnalysisSettings {
|
||||
&self.analysis_settings
|
||||
}
|
||||
|
||||
|
||||
10
ty.schema.json
generated
10
ty.schema.json
generated
@@ -208,6 +208,16 @@
|
||||
"OverrideOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"analysis": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AnalysisOptions"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": {
|
||||
"description": "A list of file and directory patterns to exclude from this override.\n\nPatterns follow a syntax similar to `.gitignore`.\nExclude patterns take precedence over include patterns within the same override.\n\nIf not specified, defaults to `[]` (excludes no files).",
|
||||
"anyOf": [
|
||||
|
||||
Reference in New Issue
Block a user