Add support for linter-specific excluded_apps
Some checks failed
Deploy Website to GitHub Pages / Deploy Website to GitHub Pages (push) Has been cancelled
ELP CI / edb (push) Has been cancelled
ELP CI / ci (26, 26.2.5.13, linux, 26.2, ubuntu-22.04, ubuntu-22.04-x64, x86_64-unknown-linux-gnu, true, linux-x64) (push) Has been cancelled
ELP CI / ci (26, 26.2.5.13, linux, 26.2, ubuntu-22.04-arm, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, true, linux-arm64) (push) Has been cancelled
ELP CI / ci (26, 26.2.5.13, macos, 26.2, macos-15-intel, macos-15-x64, x86_64-apple-darwin, true, darwin-x64) (push) Has been cancelled
ELP CI / ci (26, 26.2.5.13, macos, 26.2, macos-latest, macos-latest-arm, aarch64-apple-darwin, true, darwin-arm64) (push) Has been cancelled
ELP CI / ci (26, 26.2.5.13, windows, 26.2, windows-2022, windows-2022-x64, x86_64-pc-windows-msvc, true, win32-x64) (push) Has been cancelled
ELP CI / ci (27, 27.3.4, linux, 27.3, ubuntu-22.04, ubuntu-22.04-x64, x86_64-unknown-linux-gnu, false, linux-x64) (push) Has been cancelled
ELP CI / ci (27, 27.3.4, linux, 27.3, ubuntu-22.04-arm, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, false, linux-arm64) (push) Has been cancelled
ELP CI / ci (27, 27.3.4, macos, 27.3, macos-15-intel, macos-15-x64, x86_64-apple-darwin, false, darwin-x64) (push) Has been cancelled
ELP CI / ci (27, 27.3.4, macos, 27.3, macos-latest, macos-latest-arm, aarch64-apple-darwin, false, darwin-arm64) (push) Has been cancelled
ELP CI / ci (27, 27.3.4, windows, 27.3, windows-2022, windows-2022-x64, x86_64-pc-windows-msvc, false, win32-x64) (push) Has been cancelled
ELP CI / ci (28, 28.0.1, linux, 28, ubuntu-22.04, ubuntu-22.04-x64, x86_64-unknown-linux-gnu, false, linux-x64) (push) Has been cancelled
ELP CI / ci (28, 28.0.1, linux, 28, ubuntu-22.04-arm, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, false, linux-arm64) (push) Has been cancelled
ELP CI / ci (28, 28.0.1, macos, 28, macos-15-intel, macos-15-x64, x86_64-apple-darwin, false, darwin-x64) (push) Has been cancelled
ELP CI / ci (28, 28.0.1, macos, 28, macos-latest, macos-latest-arm, aarch64-apple-darwin, false, darwin-arm64) (push) Has been cancelled
ELP CI / ci (28, 28.0.1, windows, 28, windows-2022, windows-2022-x64, x86_64-pc-windows-msvc, false, win32-x64) (push) Has been cancelled

Summary:
We may want to disable a linter for a certain set of applications. This diff introduces such a mechanism:

```
[linters.my_linter]
exclude_apps = ["app_a", "app_2"]
```

Reviewed By: alanz

Differential Revision: D89658976

fbshipit-source-id: 78e1cf13ec22f1bf7ed1b6df6892cdad5ca2dfe5
This commit is contained in:
Roberto Aloi 2025-12-23 03:53:02 -08:00 committed by meta-codesync[bot]
parent 4bba415bc1
commit 7a4eccf14d
13 changed files with 188 additions and 13 deletions

View file

@ -2263,6 +2263,18 @@ mod tests {
);
}
#[test_case(false ; "rebar")]
#[test_case(true ; "buck")]
fn lint_linter_config_basic(buck: bool) {
simple_snapshot_sorted(
args_vec!["lint", "--read-config", "--no-stream"],
"linter_config",
expect_file!("../resources/test/linter_config/basic.stdout"),
buck,
None,
);
}
#[test_case(false ; "rebar")]
#[test_case(true ; "buck")]
fn eqwalizer_tests_check(buck: bool) {

View file

@ -0,0 +1,5 @@
Diagnostics reported:
Reporting all diagnostics codes
app_a/src/app_a.erl:3:9-3:16::[Warning] [W0002] Unused macro (MACRO_A)
app_a/src/app_a.erl:4:9-4:14::[Warning] [L1260] record rec_a is unused
app_b/src/app_b.erl:3:9-3:16::[Warning] [W0002] Unused macro (MACRO_B)

View file

@ -50,6 +50,7 @@ use elp_ide_db::text_edit::TextEdit;
use elp_ide_ssr::Match;
use elp_ide_ssr::SsrSearchScope;
use elp_ide_ssr::match_pattern;
use elp_project_model::AppName;
use elp_syntax::NodeOrToken;
use elp_syntax::Parse;
use elp_syntax::SourceFile;
@ -550,12 +551,37 @@ pub(crate) trait Linter {
}
}
fn should_process_app(
app_name: &Option<AppName>,
config: &DiagnosticsConfig,
diagnostic_code: &DiagnosticCode,
) -> bool {
let app = match app_name {
Some(app) => app.to_string(),
None => return true,
};
if let Some(lint_config) = config.lint_config.as_ref()
&& let Some(linter_config) = lint_config.linters.get(diagnostic_code)
&& let Some(ref excluded) = linter_config.exclude_apps
&& excluded.contains(&app)
{
return false;
}
true
}
fn should_run(
linter: &dyn Linter,
config: &DiagnosticsConfig,
app_name: &Option<AppName>,
is_generated: bool,
is_test: bool,
) -> bool {
if !should_process_app(app_name, config, &linter.id()) {
return false;
}
let is_enabled = if let Some(lint_config) = config.lint_config.as_ref() {
lint_config
.get_is_enabled_override(&linter.id())
@ -1218,6 +1244,16 @@ impl LintConfig {
self.linters.get(diagnostic_code)?.experimental
}
/// Get the exclude_apps override for a linter based on its diagnostic code.
pub fn get_exclude_apps_override(
&self,
diagnostic_code: &DiagnosticCode,
) -> Option<Vec<String>> {
self.linters
.get(diagnostic_code)
.and_then(|c| c.exclude_apps.clone())
}
pub fn get_function_call_linter_config(
&self,
diagnostic_code: &DiagnosticCode,
@ -1341,6 +1377,7 @@ pub struct LinterConfig {
pub include_tests: Option<bool>,
pub include_generated: Option<bool>,
pub experimental: Option<bool>,
pub exclude_apps: Option<Vec<String>>,
#[serde(flatten)]
pub config: Option<LinterTraitConfig>,
}
@ -1361,6 +1398,7 @@ impl LinterConfig {
include_tests: other.include_tests.or(self.include_tests),
include_generated: other.include_generated.or(self.include_generated),
experimental: other.experimental.or(self.experimental),
exclude_apps: other.exclude_apps.or(self.exclude_apps),
config: merged_config,
}
}
@ -1555,6 +1593,7 @@ pub fn native_diagnostics(
} else {
FxHashMap::default()
};
let app_name = db.file_app_name(file_id);
let metadata = db.elp_metadata(file_id);
// TODO: can we ever disable DiagnosticCode::SyntaxError?
// In which case we must check labeled_syntax_errors
@ -1563,6 +1602,7 @@ pub fn native_diagnostics(
&& (config.experimental && d.has_category(Category::Experimental)
|| !d.has_category(Category::Experimental))
&& !d.should_be_suppressed(&metadata, config)
&& should_process_app(&app_name, config, &d.code)
});
LabeledDiagnostics {
@ -1613,20 +1653,20 @@ pub fn diagnostics_from_descriptors(
.db
.is_test_suite_or_test_helper(file_id)
.unwrap_or(false);
let app_name = sema.db.file_app_name(file_id);
descriptors.iter().for_each(|descriptor| {
if descriptor.conditions.enabled(config, is_generated, is_test) {
if descriptor.conditions.default_disabled {
// Filter the returned diagnostics to ensure they are
// enabled
let mut diags: Vec<Diagnostic> = Vec::default();
(descriptor.checker)(&mut diags, sema, file_id, file_kind);
for diag in diags {
if config.enabled.contains(&diag.code) {
res.push(diag);
}
let mut diags: Vec<Diagnostic> = Vec::default();
(descriptor.checker)(&mut diags, sema, file_id, file_kind);
for diag in diags {
// Check if this diagnostic is enabled (for default_disabled descriptors)
// and if the app is not excluded for this diagnostic code
let is_enabled =
!descriptor.conditions.default_disabled || config.enabled.contains(&diag.code);
let app_allowed = should_process_app(&app_name, config, &diag.code);
if is_enabled && app_allowed {
res.push(diag);
}
} else {
(descriptor.checker)(res, sema, file_id, file_kind);
}
}
});
@ -1734,11 +1774,12 @@ fn diagnostics_from_linters(
.db
.is_test_suite_or_test_helper(file_id)
.unwrap_or(false);
let app_name = sema.db.file_app_name(file_id);
for l in linters {
let linter = l.as_linter();
if linter.should_process_file_id(sema, file_id)
&& should_run(linter, config, is_generated, is_test)
&& should_run(linter, config, &app_name, is_generated, is_test)
{
let severity = if let Some(lint_config) = config.lint_config.as_ref() {
lint_config
@ -2300,11 +2341,14 @@ pub fn erlang_service_diagnostics(
diags
};
let app_name = db.file_app_name(file_id);
let metadata = db.elp_metadata(file_id);
let diags = diags
.into_iter()
.filter(|(_file_id, d)| {
!d.should_be_suppressed(&metadata, config) && !config.disabled.contains(&d.code)
!d.should_be_suppressed(&metadata, config)
&& !config.disabled.contains(&d.code)
&& should_process_app(&app_name, config, &d.code)
})
.map(|(file_id, d)| {
(
@ -3995,6 +4039,7 @@ main(X) ->
include_tests: None,
include_generated: None,
experimental: None,
exclude_apps: None,
config: None,
},
);
@ -4037,6 +4082,7 @@ main(X) ->
include_tests: Some(true),
include_generated: None,
experimental: None,
exclude_apps: None,
config: None,
},
);
@ -4078,6 +4124,7 @@ main(X) ->
include_tests: None,
include_generated: Some(true),
experimental: None,
exclude_apps: None,
config: None,
},
);
@ -4120,6 +4167,7 @@ main(X) ->
include_tests: None,
include_generated: None,
experimental: Some(true),
exclude_apps: None,
config: None,
},
);
@ -4164,6 +4212,7 @@ main(X) ->
include_tests: None,
include_generated: None,
experimental: None,
exclude_apps: None,
config: None,
},
);
@ -4194,6 +4243,47 @@ main(X) ->
);
}
#[test]
fn test_linter_exclude_apps_override() {
let mut lint_config = LintConfig::default();
lint_config.linters.insert(
DiagnosticCode::NoGarbageCollect,
LinterConfig {
is_enabled: Some(false),
severity: None,
include_tests: None,
include_generated: None,
experimental: None,
exclude_apps: Some(vec!["my_app".to_string()]),
config: None,
},
);
let config = DiagnosticsConfig::default()
.configure_diagnostics(
&lint_config,
&Some("no_garbage_collect".to_string()),
&None,
FallBackToAll::No,
)
.unwrap();
check_diagnostics_with_config(
config,
r#"
//- /src/main.erl app:my_app
-module(main).
-export([warning/0]).
warning() ->
erlang:garbage_collect().
//- /opt/lib/stdlib-3.17/src/erlang.erl otp_app:/opt/lib/stdlib-3.17
-module(erlang).
-export([garbage_collect/0]).
garbage_collect() -> ok.
"#,
);
}
#[test]
fn no_unused_macro_in_macro_rhs_for_function_name() {
let config = DiagnosticsConfig::default()
@ -4241,6 +4331,7 @@ main(X) ->
include_tests: None,
include_generated: None,
experimental: None,
exclude_apps: None,
config: Some(LinterTraitConfig::FunctionCallLinterConfig(
FunctionCallLinterConfig {
include: Some(vec![FunctionMatch::mf("mod_a", "func_a")]),
@ -4273,6 +4364,7 @@ main(X) ->
include_tests: Some(true),
include_generated: None,
experimental: None,
exclude_apps: None,
config: Some(LinterTraitConfig::FunctionCallLinterConfig(
FunctionCallLinterConfig {
include: Some(vec![FunctionMatch::mf("mod_b", "func_b")]),
@ -4290,6 +4382,7 @@ main(X) ->
include_tests: None,
include_generated: Some(true),
experimental: None,
exclude_apps: None,
config: None,
},
);

View file

@ -0,0 +1,5 @@
[buck]
enabled = true
build_deps = false
included_targets = [ "root//linter_config/..." ]
source_root = "linter_config"

View file

@ -0,0 +1,5 @@
[linters.L1260] # Unused record, produced by the Erlang Service
exclude_apps = ["app_b", "app_c"]
[linters.unused_macro]
exclude_apps = ["app_c"]

View file

@ -0,0 +1,25 @@
oncall("vscode_erlang")
erlang_application(
name = "app_a",
srcs = glob(["app_a/src/*.erl"]),
app_src = "app_a/src/app_a.app.src",
labels = ["user_application"],
version = "1.0.0",
)
erlang_application(
name = "app_b",
srcs = glob(["app_b/src/*.erl"]),
app_src = "app_b/src/app_b.app.src",
labels = ["user_application"],
version = "1.0.0",
)
erlang_application(
name = "app_c",
srcs = glob(["app_c/src/*.erl"]),
app_src = "app_c/src/app_c.app.src",
labels = ["user_application"],
version = "1.0.0",
)

View file

@ -0,0 +1,3 @@
{application, app_a,
[{description, "example app A"}, {vsn, "inplace"}, {applications, [kernel, stdlib]}]
}.

View file

@ -0,0 +1,4 @@
-module(app_a).
-define(MACRO_A, a).
-record(rec_a, {a :: atom()}).

View file

@ -0,0 +1,3 @@
{application, app_b,
[{description, "example app B"}, {vsn, "inplace"}, {applications, [kernel, stdlib]}]
}.

View file

@ -0,0 +1,4 @@
-module(app_b).
-define(MACRO_B, b).
-record(rec_b, {b :: atom()}).

View file

@ -0,0 +1,3 @@
{application, app_c,
[{description, "example app C"}, {vsn, "inplace"}, {applications, [kernel, stdlib]}]
}.

View file

@ -0,0 +1,4 @@
-module(app_c).
-define(MACRO_C, c).
-record(rec_c, {c :: atom()}).

View file

@ -0,0 +1,9 @@
{checkouts_dir, ["."]}.
{project_app_dirs, [
"app_a",
"app_b",
"app_c"
]}.
{erl_opts, [debug_info]}.
{deps, []}.