[ty] Support overriding respect-type-ignore-comments (#22615)

This commit is contained in:
Micha Reiser
2026-01-19 09:09:42 +01:00
committed by GitHub
parent c50863a0ee
commit 65b7fc9e73
12 changed files with 303 additions and 34 deletions

View File

@@ -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`

View File

@@ -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(())
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
View File

@@ -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": [