Skip to content

Commit fec1192

Browse files
alanzmeta-codesync[bot]
authored andcommitted
Add elp ssr command
Summary: We have the ability to specifiy an AdHoc diagnostic in a config file, from the diff titled `[ELP] Specify SSR match as Ad-Hoc diagnostic`. We now simplify usage by allowing it to be given on the command line too, so a simple ``` elp ssr "{_A, _B}" ``` will find and report all 2 element tuples in the project. Note: the full syntax for ssr can also be used if guards are required, e.g. ``` elp ssr "ssr: {_X = _Y} when _X == foo." ``` Reviewed By: michalmuskala Differential Revision: D85443930 fbshipit-source-id: 108e2a583437fb3a6e702c21aef874395b376e7b
1 parent 46477ee commit fec1192

File tree

9 files changed

+253
-1
lines changed

9 files changed

+253
-1
lines changed

crates/elp/src/bin/args.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,50 @@ pub struct Lint {
332332
pub ignore_apps: Vec<String>,
333333
}
334334

335+
#[derive(Clone, Debug, Bpaf)]
336+
pub struct Ssr {
337+
/// Path to directory with project, or to a JSON file (defaults to `.`)
338+
#[bpaf(argument("PROJECT"), fallback(PathBuf::from(".")))]
339+
pub project: PathBuf,
340+
/// Parse a single module from the project, not the entire project.
341+
#[bpaf(argument("MODULE"))]
342+
pub module: Option<String>,
343+
/// Parse a single application from the project, not the entire project.
344+
#[bpaf(long("app"), long("application"), argument("APP"))]
345+
pub app: Option<String>,
346+
/// Parse a single file from the project, not the entire project. This can be an include file or escript, etc.
347+
#[bpaf(argument("FILE"))]
348+
pub file: Option<String>,
349+
350+
/// Run with rebar
351+
pub rebar: bool,
352+
/// Rebar3 profile to pickup (default is test)
353+
#[bpaf(long("as"), argument("PROFILE"), fallback("test".to_string()))]
354+
pub profile: String,
355+
356+
/// Also generate diagnostics for generated files
357+
pub include_generated: bool,
358+
/// Also generate diagnostics for test files
359+
pub include_tests: bool,
360+
361+
/// Show diagnostics in JSON format
362+
#[bpaf(
363+
argument("FORMAT"),
364+
complete(format_completer),
365+
fallback(None),
366+
guard(format_guard, "Please use json")
367+
)]
368+
pub format: Option<String>,
369+
370+
/// Report system memory usage and other statistics
371+
#[bpaf(long("report-system-stats"))]
372+
pub report_system_stats: bool,
373+
374+
/// SSR spec to use
375+
#[bpaf(positional("SSR_SPEC"))]
376+
pub ssr_spec: String,
377+
}
378+
335379
#[derive(Clone, Debug, Bpaf)]
336380
pub struct Explain {
337381
/// Error code to explain
@@ -397,6 +441,7 @@ pub enum Command {
397441
GenerateCompletions(GenerateCompletions),
398442
RunServer(RunServer),
399443
Lint(Lint),
444+
Ssr(Ssr),
400445
Version(Version),
401446
Shell(Shell),
402447
Explain(Explain),
@@ -507,6 +552,12 @@ pub fn command() -> impl Parser<Command> {
507552
.command("lint")
508553
.help("Parse files in project and emit diagnostics, optionally apply fixes.");
509554

555+
let ssr = ssr()
556+
.map(Command::Ssr)
557+
.to_options()
558+
.command("ssr")
559+
.help("Run SSR (Structural Search and Replace) pattern matching on project files.");
560+
510561
let run_server = run_server()
511562
.map(Command::RunServer)
512563
.to_options()
@@ -556,6 +607,7 @@ pub fn command() -> impl Parser<Command> {
556607
eqwalize_target,
557608
dialyze_all,
558609
lint,
610+
ssr,
559611
run_server,
560612
generate_completions,
561613
parse_all,

crates/elp/src/bin/lint_cli.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fn get_and_report_diagnostics_config(args: &Lint, cli: &mut dyn Cli) -> Result<D
109109
Ok(diagnostics_config)
110110
}
111111

112-
fn load_project(
112+
pub fn load_project(
113113
args: &Lint,
114114
cli: &mut dyn Cli,
115115
query_config: &BuckQueryConfig,
@@ -396,6 +396,7 @@ fn get_diagnostics_config(args: &Lint) -> Result<DiagnosticsConfig> {
396396
} else {
397397
LintConfig::default()
398398
};
399+
399400
let cfg = DiagnosticsConfig::default()
400401
.configure_diagnostics(
401402
&cfg_from_file,

crates/elp/src/bin/main.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ mod lint_cli;
4343
// @fb-only
4444
mod reporting;
4545
mod shell;
46+
mod ssr_cli;
4647

4748
// Use jemalloc as the global allocator
4849
#[cfg(not(any(target_env = "msvc", target_os = "openbsd")))]
@@ -135,6 +136,7 @@ fn try_main(cli: &mut dyn Cli, args: Args) -> Result<()> {
135136
args::Command::BuildInfo(args) => build_info_cli::save_build_info(args, &query_config)?,
136137
args::Command::ProjectInfo(args) => build_info_cli::save_project_info(args, &query_config)?,
137138
args::Command::Lint(args) => lint_cli::run_lint_command(&args, cli, &query_config)?,
139+
args::Command::Ssr(args) => ssr_cli::run_ssr_command(&args, cli, &query_config)?,
138140
args::Command::GenerateCompletions(args) => {
139141
let instructions = args::gen_completions(&args.shell);
140142
writeln!(cli, "#Please run this:\n{instructions}")?
@@ -2000,6 +2002,40 @@ mod tests {
20002002
)
20012003
}
20022004

2005+
#[test]
2006+
fn lint_ssr_as_cli_arg() {
2007+
simple_snapshot(
2008+
args_vec!["ssr", "ssr: {_@A, _@B}.",],
2009+
"linter",
2010+
expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"),
2011+
true,
2012+
None,
2013+
)
2014+
}
2015+
2016+
#[test]
2017+
fn lint_ssr_as_cli_arg_without_prefix() {
2018+
simple_snapshot(
2019+
args_vec!["ssr", "{_@A, _@B}",],
2020+
"linter",
2021+
expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"),
2022+
true,
2023+
None,
2024+
)
2025+
}
2026+
2027+
#[test]
2028+
fn lint_ssr_as_cli_arg_malformed() {
2029+
simple_snapshot_expect_stderror(
2030+
args_vec!["ssr", "ssr: {_@A, = _@B}.",],
2031+
"linter",
2032+
expect_file!("../resources/test/linter/ssr_ad_hoc_cli_parse_error.stdout"),
2033+
true,
2034+
None,
2035+
false,
2036+
)
2037+
}
2038+
20032039
#[test_case(false ; "rebar")]
20042040
#[test_case(true ; "buck")]
20052041
fn eqwalizer_tests_check(buck: bool) {
@@ -2221,6 +2257,16 @@ mod tests {
22212257
expected.assert_eq(&stdout);
22222258
}
22232259

2260+
#[test]
2261+
fn ssr_help() {
2262+
let args = args::args()
2263+
.run_inner(Args::from(&["ssr", "--help"]))
2264+
.unwrap_err();
2265+
let expected = expect_file!["../resources/test/ssr_help.stdout"];
2266+
let stdout = args.unwrap_stdout();
2267+
expected.assert_eq(&stdout);
2268+
}
2269+
22242270
#[test]
22252271
fn build_info_help() {
22262272
let args = args::args()

crates/elp/src/bin/ssr_cli.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is dual-licensed under either the MIT license found in the
5+
* LICENSE-MIT file in the root directory of this source tree or the Apache
6+
* License, Version 2.0 found in the LICENSE-APACHE file in the root directory
7+
* of this source tree. You may select, at your option, one of the
8+
* above-listed licenses.
9+
*/
10+
11+
use anyhow::Result;
12+
use anyhow::bail;
13+
use elp::cli::Cli;
14+
use elp_ide::AnalysisHost;
15+
use elp_ide::diagnostics;
16+
use elp_ide::diagnostics::DiagnosticsConfig;
17+
use elp_ide::diagnostics::FallBackToAll;
18+
use elp_ide::diagnostics::LintConfig;
19+
use elp_ide::diagnostics::MatchSsr;
20+
use elp_project_model::buck::BuckQueryConfig;
21+
22+
use crate::args::Ssr;
23+
use crate::lint_cli;
24+
25+
fn normalize_ssr_pattern(pattern: &str) -> String {
26+
if pattern.starts_with("ssr:") {
27+
pattern.to_string()
28+
} else {
29+
format!("ssr: {}.", pattern)
30+
}
31+
}
32+
33+
pub fn run_ssr_command(
34+
args: &Ssr,
35+
cli: &mut dyn Cli,
36+
query_config: &BuckQueryConfig,
37+
) -> Result<()> {
38+
// Normalize the SSR pattern
39+
let normalized_pattern = normalize_ssr_pattern(&args.ssr_spec);
40+
41+
// Validate the SSR pattern early
42+
let analysis_host = AnalysisHost::default();
43+
let analysis = analysis_host.analysis();
44+
match analysis.validate_ssr_pattern(&normalized_pattern) {
45+
Ok(Ok(())) => {}
46+
Ok(Err(e)) => bail!("invalid SSR pattern '{}': {}", args.ssr_spec, e),
47+
Err(_cancelled) => bail!("SSR pattern validation was cancelled"),
48+
}
49+
50+
// Create the lint config with the SSR pattern
51+
let mut lint_config = LintConfig::default();
52+
let ssr_lint = diagnostics::Lint::LintMatchSsr(MatchSsr {
53+
ssr_pattern: normalized_pattern,
54+
message: None,
55+
});
56+
lint_config.ad_hoc_lints.lints.push(ssr_lint);
57+
58+
// Build the diagnostics config
59+
let diagnostics_config = DiagnosticsConfig::default()
60+
.configure_diagnostics(
61+
&lint_config,
62+
&Some("ad-hoc: ssr-match".to_string()),
63+
&None,
64+
FallBackToAll::Yes,
65+
)?
66+
.set_include_generated(args.include_generated)
67+
.set_experimental(false)
68+
.set_use_cli_severity(false);
69+
70+
if diagnostics_config.enabled.all_enabled() && args.is_format_normal() {
71+
writeln!(cli, "Reporting all diagnostics codes")?;
72+
}
73+
74+
// Convert Ssr args to Lint args to reuse lint_cli functionality
75+
let lint_args = crate::args::Lint {
76+
project: args.project.clone(),
77+
module: args.module.clone(),
78+
app: args.app.clone(),
79+
file: args.file.clone(),
80+
rebar: args.rebar,
81+
profile: args.profile.clone(),
82+
include_generated: args.include_generated,
83+
include_tests: args.include_tests,
84+
print_diags: true,
85+
format: args.format.clone(),
86+
prefix: None,
87+
include_erlc_diagnostics: false,
88+
include_ct_diagnostics: false,
89+
include_edoc_diagnostics: false,
90+
include_eqwalizer_diagnostics: false,
91+
include_suppressed: false,
92+
use_cli_severity: false,
93+
diagnostic_ignore: None,
94+
diagnostic_filter: Some("ad-hoc: ssr-match".to_string()),
95+
experimental_diags: false,
96+
read_config: false,
97+
config_file: None,
98+
apply_fix: false,
99+
ignore_fix_only: false,
100+
in_place: false,
101+
to: None,
102+
recursive: false,
103+
with_check: false,
104+
check_eqwalize_all: false,
105+
one_shot: false,
106+
report_system_stats: args.report_system_stats,
107+
ignore_apps: vec![],
108+
};
109+
110+
// Load the project
111+
let mut loaded = lint_cli::load_project(&lint_args, cli, query_config)?;
112+
113+
// Run the codemod with the SSR pattern
114+
lint_cli::do_codemod(cli, &mut loaded, &diagnostics_config, &lint_args)
115+
}
116+
117+
impl Ssr {
118+
pub fn is_format_normal(&self) -> bool {
119+
self.format.is_none()
120+
}
121+
}

crates/elp/src/resources/test/help.stdout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Available commands:
1616
eqwalize-target Eqwalize all opted-in modules in specified buck target
1717
dialyze-all Run Dialyzer on the whole project by shelling out to a `dialyzer-run` tool on the path to do the legwork.
1818
lint Parse files in project and emit diagnostics, optionally apply fixes.
19+
ssr Run SSR (Structural Search and Replace) pattern matching on project files.
1920
server Run lsp server
2021
generate-completions Generate shell completions
2122
parse-all Dump ast for all files in a project for specified rebar.config file
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
invalid SSR pattern 'ssr: {_@A, = _@B}.': Parse error: Could not lower rule
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Usage: [--project PROJECT] [--module MODULE] [--app APP] [--file FILE] [--rebar] [--as PROFILE] [--include-generated] [--include-tests] [[--format FORMAT]] [--report-system-stats] <SSR_SPEC>
2+
3+
Available positional items:
4+
<SSR_SPEC> SSR spec to use
5+
6+
Available options:
7+
--project <PROJECT> Path to directory with project, or to a JSON file (defaults to `.`)
8+
--module <MODULE> Parse a single module from the project, not the entire project.
9+
--app <APP> Parse a single application from the project, not the entire project.
10+
--file <FILE> Parse a single file from the project, not the entire project. This can be an include file or escript, etc.
11+
--rebar Run with rebar
12+
--as <PROFILE> Rebar3 profile to pickup (default is test)
13+
--include-generated Also generate diagnostics for generated files
14+
--include-tests Also generate diagnostics for test files
15+
--format <FORMAT> Show diagnostics in JSON format
16+
--report-system-stats Report system memory usage and other statistics
17+
-h, --help Prints help information

crates/ide/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,16 @@ impl Analysis {
566566
self.with_db(|db| db.is_otp(file_id))
567567
}
568568

569+
/// Validates an SSR pattern string by attempting to parse it.
570+
/// Returns `Ok(())` if the pattern is valid, or an error message if parsing fails.
571+
pub fn validate_ssr_pattern(&self, pattern: &str) -> Cancellable<Result<(), String>> {
572+
self.with_db(|db| {
573+
elp_ide_ssr::SsrRule::parse_str(db, pattern)
574+
.map(|_| ())
575+
.map_err(|e| e.to_string())
576+
})
577+
}
578+
569579
/// Search symbols. Only module names are currently supported.
570580
pub fn symbol_search(
571581
&self,

0 commit comments

Comments
 (0)