Skip to content

Commit 90281d7

Browse files
alanzmeta-codesync[bot]
authored andcommitted
Specify SSR match as Ad-Hoc diagnostic
Summary: Structured Search (and not yet Replace) SSR is a way of specifying a "semantic grep", which searches by comparing the AST rather than text. We use it as a building block for diagnostics. But often we want to just do a quick search for a pattern in a file or code base. This diff allows us to do this, by specifying it in a config file. Reviewed By: TD5 Differential Revision: D85340131 fbshipit-source-id: f1e4ce515a535cc6032483dc39297eab4e6eecd5
1 parent b39d8f0 commit 90281d7

File tree

9 files changed

+190
-1
lines changed

9 files changed

+190
-1
lines changed

crates/elp/src/bin/lint_cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,7 @@ mod tests {
993993
expect![[r#"
994994
enabled_lints = ["P1700"]
995995
disabled_lints = []
996-
996+
997997
[erlang_service]
998998
warnings_as_errors = true
999999
[[ad_hoc_lints.lints]]

crates/elp/src/bin/main.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,37 @@ mod tests {
19381938
)
19391939
}
19401940

1941+
#[test]
1942+
fn lint_ssr_from_config() {
1943+
simple_snapshot(
1944+
args_vec![
1945+
"lint",
1946+
"--config-file",
1947+
"../../test_projects/linter/elp_lint_ssr_adhoc.toml",
1948+
],
1949+
"linter",
1950+
expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"),
1951+
true,
1952+
None,
1953+
)
1954+
}
1955+
1956+
#[test]
1957+
fn lint_ssr_from_bad_config() {
1958+
simple_snapshot_expect_stderror(
1959+
args_vec![
1960+
"lint",
1961+
"--config-file",
1962+
"../../test_projects/linter/elp_lint_ssr_adhoc_parse_fail.toml",
1963+
],
1964+
"linter",
1965+
expect_file!("../resources/test/linter/ssr_ad_hoc_parse_fail.stdout"),
1966+
true,
1967+
None,
1968+
false,
1969+
)
1970+
}
1971+
19411972
#[test_case(false ; "rebar")]
19421973
#[test_case(true ; "buck")]
19431974
fn eqwalizer_tests_check(buck: bool) {

crates/elp/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ mod tests {
162162
use elp_ide::diagnostics::ErlangServiceConfig;
163163
use elp_ide::diagnostics::Lint;
164164
use elp_ide::diagnostics::LintsFromConfig;
165+
use elp_ide::diagnostics::MatchSsr;
165166
use elp_ide::diagnostics::ReplaceCall;
166167
use elp_ide::diagnostics::ReplaceCallAction;
167168
use elp_ide::diagnostics::Replacement;
@@ -188,6 +189,10 @@ mod tests {
188189
perm: vec![1, 2],
189190
}),
190191
}),
192+
Lint::LintMatchSsr(MatchSsr {
193+
ssr_pattern: "ssr: _@A = 10.".to_string(),
194+
message: None,
195+
}),
191196
],
192197
},
193198
linters: FxHashMap::default(),
@@ -223,6 +228,10 @@ mod tests {
223228
type = "ArgsPermutation"
224229
perm = [1, 2]
225230
231+
[[ad_hoc_lints.lints]]
232+
type = "LintMatchSsr"
233+
ssr_pattern = "ssr: _@A = 10."
234+
226235
[linters]
227236
"#]]
228237
.assert_eq(&toml::to_string::<LintConfig>(&lint_config).unwrap());
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Diagnostics reported in 1 modules:
2+
app_a: 1
3+
15:12-15:17::[WeakWarning] [ad-hoc: ssr-match] SSR pattern matched: ssr: {_@A, _@B}.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
errors parsing "{project_path}/elp_lint_ssr_adhoc_parse_fail.toml": invalid SSR pattern 'ssr: {_@A, = 10}.': Parse error: Could not lower rule for key `ad_hoc_lints.lints` at line 3 column 1

crates/ide/src/diagnostics.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ mod unused_record_field;
149149
pub use elp_ide_db::DiagnosticCode;
150150
pub use from_config::Lint;
151151
pub use from_config::LintsFromConfig;
152+
pub use from_config::MatchSsr;
152153
pub use from_config::ReplaceCall;
153154
pub use from_config::ReplaceCallAction;
154155
pub use replace_call::Replacement;

crates/ide/src/diagnostics/from_config.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,21 @@
1010

1111
//! Read a section of the config file and generate diagnostics from it.
1212
13+
use elp_ide_db::DiagnosticCode;
14+
use elp_ide_db::RootDatabase;
1315
use elp_ide_db::elp_base_db::FileId;
16+
use elp_ide_ssr::SsrRule;
17+
use elp_ide_ssr::SsrSearchScope;
18+
use elp_ide_ssr::match_pattern;
1419
use hir::Semantic;
20+
use hir::Strategy;
21+
use hir::fold::MacroStrategy;
22+
use hir::fold::ParenStrategy;
1523
use serde::Deserialize;
1624
use serde::Serialize;
1725

1826
use super::Diagnostic;
27+
use super::Severity;
1928
use super::TypeReplacement;
2029
use super::replace_call;
2130
use super::replace_call::Replacement;
@@ -41,13 +50,15 @@ impl LintsFromConfig {
4150
pub enum Lint {
4251
ReplaceCall(ReplaceCall),
4352
ReplaceInSpec(ReplaceInSpec),
53+
LintMatchSsr(MatchSsr),
4454
}
4555

4656
impl Lint {
4757
pub fn get_diagnostics(&self, acc: &mut Vec<Diagnostic>, sema: &Semantic, file_id: FileId) {
4858
match self {
4959
Lint::ReplaceCall(l) => l.get_diagnostics(acc, sema, file_id),
5060
Lint::ReplaceInSpec(l) => l.get_diagnostics(acc, sema, file_id),
61+
Lint::LintMatchSsr(l) => l.get_diagnostics(acc, sema, file_id),
5162
}
5263
}
5364
}
@@ -115,12 +126,82 @@ impl ReplaceInSpec {
115126

116127
// ---------------------------------------------------------------------
117128

129+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
130+
pub struct MatchSsr {
131+
pub ssr_pattern: String,
132+
#[serde(default)]
133+
pub message: Option<String>,
134+
}
135+
136+
impl<'de> Deserialize<'de> for MatchSsr {
137+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138+
where
139+
D: serde::Deserializer<'de>,
140+
{
141+
use serde::de::Error;
142+
143+
#[derive(Deserialize)]
144+
struct MatchSsrHelper {
145+
ssr_pattern: String,
146+
#[serde(default)]
147+
message: Option<String>,
148+
}
149+
150+
let helper = MatchSsrHelper::deserialize(deserializer)?;
151+
152+
// Validate the SSR pattern by trying to parse it
153+
// Use a minimal database for validation
154+
let db = RootDatabase::default();
155+
SsrRule::parse_str(&db, &helper.ssr_pattern).map_err(|e| {
156+
D::Error::custom(format!(
157+
"invalid SSR pattern '{}': {}",
158+
helper.ssr_pattern, e
159+
))
160+
})?;
161+
162+
Ok(MatchSsr {
163+
ssr_pattern: helper.ssr_pattern,
164+
message: helper.message,
165+
})
166+
}
167+
}
168+
169+
impl MatchSsr {
170+
pub fn get_diagnostics(&self, acc: &mut Vec<Diagnostic>, sema: &Semantic, file_id: FileId) {
171+
let strategy = Strategy {
172+
macros: MacroStrategy::Expand,
173+
parens: ParenStrategy::InvisibleParens,
174+
};
175+
176+
let scope = SsrSearchScope::WholeFile(file_id);
177+
let matches = match_pattern(sema, strategy, &self.ssr_pattern, scope);
178+
179+
for matched in matches.matches {
180+
let message = self
181+
.message
182+
.clone()
183+
.unwrap_or_else(|| format!("SSR pattern matched: {}", self.ssr_pattern));
184+
185+
let diag = Diagnostic::new(
186+
DiagnosticCode::AdHoc("ssr-match".to_string()),
187+
message,
188+
matched.range.range,
189+
)
190+
.with_severity(Severity::WeakWarning);
191+
acc.push(diag);
192+
}
193+
}
194+
}
195+
196+
// ---------------------------------------------------------------------
197+
118198
#[cfg(test)]
119199
mod tests {
120200
use expect_test::expect;
121201

122202
use super::Lint;
123203
use super::LintsFromConfig;
204+
use super::MatchSsr;
124205
use super::ReplaceCall;
125206
use super::ReplaceCallAction;
126207
use super::ReplaceInSpec;
@@ -492,4 +573,57 @@ mod tests {
492573
"#]]
493574
.assert_eq(&result);
494575
}
576+
577+
#[test]
578+
fn serde_serialize_match_ssr() {
579+
let result = toml::to_string::<MatchSsr>(&MatchSsr {
580+
ssr_pattern: "ssr: _@A = 10.".to_string(),
581+
message: Some("Found pattern".to_string()),
582+
})
583+
.unwrap();
584+
expect![[r#"
585+
ssr_pattern = "ssr: _@A = 10."
586+
message = "Found pattern"
587+
"#]]
588+
.assert_eq(&result);
589+
}
590+
591+
#[test]
592+
fn serde_deserialize_match_ssr() {
593+
let match_ssr: MatchSsr = toml::from_str(
594+
r#"
595+
ssr_pattern = "ssr: _@A = 10."
596+
message = "Found pattern"
597+
"#,
598+
)
599+
.unwrap();
600+
601+
expect![[r#"
602+
MatchSsr {
603+
ssr_pattern: "ssr: _@A = 10.",
604+
message: Some(
605+
"Found pattern",
606+
),
607+
}
608+
"#]]
609+
.assert_debug_eq(&match_ssr);
610+
}
611+
612+
#[test]
613+
fn serde_serialize_lint_match_ssr() {
614+
let result = toml::to_string::<LintsFromConfig>(&LintsFromConfig {
615+
lints: vec![Lint::LintMatchSsr(MatchSsr {
616+
ssr_pattern: "ssr: _@A = 10.".to_string(),
617+
message: Some("Found pattern".to_string()),
618+
})],
619+
})
620+
.unwrap();
621+
expect![[r#"
622+
[[lints]]
623+
type = "LintMatchSsr"
624+
ssr_pattern = "ssr: _@A = 10."
625+
message = "Found pattern"
626+
"#]]
627+
.assert_eq(&result);
628+
}
495629
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enabled_lints = ["ad-hoc: ssr-match"]
2+
3+
[[ad_hoc_lints.lints]]
4+
type = "LintMatchSsr"
5+
ssr_pattern = "ssr: {_@A, _@B}."
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enabled_lints = ["ad-hoc: ssr-match"]
2+
3+
[[ad_hoc_lints.lints]]
4+
type = "LintMatchSsr"
5+
ssr_pattern = "ssr: {_@A, = 10}."

0 commit comments

Comments
 (0)