88 * above-listed licenses.
99 */
1010
11+ use std:: fs;
12+ use std:: path:: Path ;
13+ use std:: str;
14+ use std:: time:: SystemTime ;
15+
1116use anyhow:: Result ;
1217use anyhow:: bail;
18+ use elp:: build:: load;
19+ use elp:: build:: types:: LoadResult ;
1320use elp:: cli:: Cli ;
21+ use elp:: convert;
22+ use elp:: memory_usage:: MemoryUsage ;
23+ use elp:: otp_file_to_ignore;
24+ use elp_eqwalizer:: Mode ;
25+ use elp_ide:: Analysis ;
1426use elp_ide:: AnalysisHost ;
1527use elp_ide:: diagnostics;
1628use elp_ide:: diagnostics:: DiagnosticsConfig ;
1729use elp_ide:: diagnostics:: FallBackToAll ;
1830use elp_ide:: diagnostics:: LintConfig ;
1931use elp_ide:: diagnostics:: MatchSsr ;
32+ use elp_ide:: elp_ide_db:: elp_base_db:: AbsPath ;
33+ use elp_ide:: elp_ide_db:: elp_base_db:: FileId ;
34+ use elp_ide:: elp_ide_db:: elp_base_db:: IncludeOtp ;
35+ use elp_ide:: elp_ide_db:: elp_base_db:: ModuleName ;
36+ use elp_ide:: elp_ide_db:: elp_base_db:: ProjectId ;
37+ use elp_ide:: elp_ide_db:: elp_base_db:: VfsPath ;
38+ use elp_log:: telemetry;
39+ use elp_project_model:: AppName ;
40+ use elp_project_model:: AppType ;
41+ use elp_project_model:: DiscoverConfig ;
2042use elp_project_model:: buck:: BuckQueryConfig ;
43+ use hir:: Semantic ;
44+ use indicatif:: ParallelProgressIterator ;
45+ use paths:: Utf8PathBuf ;
46+ use rayon:: prelude:: ParallelBridge ;
47+ use rayon:: prelude:: ParallelIterator ;
2148
2249use crate :: args:: Ssr ;
23- use crate :: lint_cli;
50+ use crate :: reporting;
51+ use crate :: reporting:: print_memory_usage;
2452
2553fn normalize_ssr_pattern ( pattern : & str ) -> String {
2654 if pattern. starts_with ( "ssr:" ) {
@@ -35,6 +63,8 @@ pub fn run_ssr_command(
3563 cli : & mut dyn Cli ,
3664 query_config : & BuckQueryConfig ,
3765) -> Result < ( ) > {
66+ let start_time = SystemTime :: now ( ) ;
67+ let memory_start = MemoryUsage :: now ( ) ;
3868 // Normalize the SSR pattern
3969 let normalized_pattern = normalize_ssr_pattern ( & args. ssr_spec ) ;
4070
@@ -71,51 +101,224 @@ pub fn run_ssr_command(
71101 writeln ! ( cli, "Reporting all diagnostics codes" ) ?;
72102 }
73103
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 ! [ ] ,
104+ // Load the project
105+ let mut loaded = load_project ( args, cli, query_config) ?;
106+ telemetry:: report_elapsed_time ( "ssr operational" , start_time) ;
107+
108+ let r = run_ssr ( cli, & mut loaded, & diagnostics_config, args) ;
109+
110+ telemetry:: report_elapsed_time ( "ssr done" , start_time) ;
111+
112+ let memory_end = MemoryUsage :: now ( ) ;
113+ let memory_used = memory_end - memory_start;
114+
115+ // Print memory usage at the end if requested and format is normal
116+ if args. is_format_normal ( ) && args. report_system_stats {
117+ print_memory_usage ( loaded. analysis_host , loaded. vfs , cli) ?;
118+ writeln ! ( cli, "{}" , memory_used) ?;
119+ }
120+ r
121+ }
122+
123+ pub fn run_ssr (
124+ cli : & mut dyn Cli ,
125+ loaded : & mut LoadResult ,
126+ diagnostics_config : & DiagnosticsConfig ,
127+ args : & Ssr ,
128+ ) -> Result < ( ) > {
129+ let analysis = loaded. analysis ( ) ;
130+ let ( file_id, name) = match & args. module {
131+ Some ( module) => match analysis. module_file_id ( loaded. project_id , module) ? {
132+ Some ( file_id) => {
133+ if args. is_format_normal ( ) {
134+ writeln ! ( cli, "module specified: {module}" ) ?;
135+ }
136+ ( Some ( file_id) , analysis. module_name ( file_id) ?)
137+ }
138+ None => panic ! ( "Module not found: {module}" ) ,
139+ } ,
140+ None => match & args. file {
141+ Some ( file_name) => {
142+ if args. is_format_normal ( ) {
143+ writeln ! ( cli, "file specified: {file_name}" ) ?;
144+ }
145+ let path_buf = Utf8PathBuf :: from_path_buf ( fs:: canonicalize ( file_name) . unwrap ( ) )
146+ . expect ( "UTF8 conversion failed" ) ;
147+ let path = AbsPath :: assert ( & path_buf) ;
148+ let path = path. as_os_str ( ) . to_str ( ) . unwrap ( ) ;
149+ (
150+ loaded
151+ . vfs
152+ . file_id ( & VfsPath :: new_real_path ( path. to_string ( ) ) )
153+ . map ( |( id, _) | id) ,
154+ path_buf. as_path ( ) . file_name ( ) . map ( ModuleName :: new) ,
155+ )
156+ }
157+ None => ( None , None ) ,
158+ } ,
108159 } ;
109160
110- // Load the project
111- let mut loaded = lint_cli:: load_project ( & lint_args, cli, query_config) ?;
161+ let mut diags = match ( file_id, name) {
162+ ( None , _) => do_parse_all ( cli, & analysis, & loaded. project_id , diagnostics_config, args) ?,
163+ ( Some ( file_id) , Some ( name) ) => {
164+ if let Some ( app) = & args. app
165+ && let Ok ( Some ( file_app) ) = analysis. file_app_name ( file_id)
166+ && file_app != AppName ( app. to_string ( ) )
167+ {
168+ panic ! ( "Module {} does not belong to app {}" , name. as_str( ) , app)
169+ }
170+ do_parse_one ( & analysis, diagnostics_config, file_id, & name, args) ?
171+ . map_or ( vec ! [ ] , |x| vec ! [ x] )
172+ }
173+ ( Some ( file_id) , _) => {
174+ panic ! ( "Could not get name from file_id for {file_id:?}" )
175+ }
176+ } ;
177+ if diags. is_empty ( ) {
178+ if args. is_format_normal ( ) {
179+ writeln ! ( cli, "No matches found" ) ?;
180+ }
181+ } else {
182+ diags. sort_by ( |( a, _, _) , ( b, _, _) | a. cmp ( b) ) ;
183+ if args. is_format_json ( ) {
184+ for ( _name, file_id, diags) in & diags {
185+ for diag in diags {
186+ // We use JSON output for CI, and want to see warnings too.
187+ // So do not filter on errors only
188+ let vfs_path = loaded. vfs . file_path ( * file_id) ;
189+ let analysis = loaded. analysis ( ) ;
190+ let root_path = & analysis
191+ . project_data ( * file_id)
192+ . unwrap_or_else ( |_err| panic ! ( "could not find project data" ) )
193+ . unwrap_or_else ( || panic ! ( "could not find project data" ) )
194+ . root_dir ;
195+ let relative_path = reporting:: get_relative_path ( root_path, vfs_path) ;
196+ print_diagnostic_json ( diag, & analysis, * file_id, relative_path, false , cli) ?;
197+ }
198+ }
199+ } else {
200+ writeln ! ( cli, "Matches found in {} modules:" , diags. len( ) ) ?;
201+ for ( name, file_id, diags) in & diags {
202+ writeln ! ( cli, " {}: {}" , name, diags. len( ) ) ?;
203+ for diag in diags {
204+ print_diagnostic ( diag, & loaded. analysis ( ) , * file_id, false , cli) ?;
205+ }
206+ }
207+ }
208+ }
209+ Ok ( ( ) )
210+ }
211+
212+ fn load_project (
213+ args : & Ssr ,
214+ cli : & mut dyn Cli ,
215+ query_config : & BuckQueryConfig ,
216+ ) -> Result < LoadResult > {
217+ log:: info!( "Loading project at: {:?}" , args. project) ;
218+ let config = DiscoverConfig :: new ( args. rebar , & args. profile ) ;
219+ load:: load_project_at (
220+ cli,
221+ & args. project ,
222+ config,
223+ IncludeOtp :: Yes ,
224+ Mode :: Server ,
225+ query_config,
226+ )
227+ }
112228
113- // Run the codemod with the SSR pattern
114- lint_cli:: do_codemod ( cli, & mut loaded, & diagnostics_config, & lint_args)
229+ fn do_parse_all (
230+ cli : & dyn Cli ,
231+ analysis : & Analysis ,
232+ project_id : & ProjectId ,
233+ config : & DiagnosticsConfig ,
234+ args : & Ssr ,
235+ ) -> Result < Vec < ( String , FileId , Vec < diagnostics:: Diagnostic > ) > > {
236+ let module_index = analysis. module_index ( * project_id) . unwrap ( ) ;
237+ let module_iter = module_index. iter_own ( ) ;
238+
239+ let pb = cli. progress ( module_iter. len ( ) as u64 , "Scanning modules" ) ;
240+ let app_name = args. app . as_ref ( ) . map ( |name| AppName ( name. to_string ( ) ) ) ;
241+
242+ Ok ( module_iter
243+ . par_bridge ( )
244+ . progress_with ( pb)
245+ . map_with (
246+ analysis. clone ( ) ,
247+ |db, ( module_name, _file_source, file_id) | {
248+ if !otp_file_to_ignore ( db, file_id)
249+ && db. file_app_type ( file_id) . ok ( ) != Some ( Some ( AppType :: Dep ) )
250+ && ( app_name. is_none ( )
251+ || db. file_app_name ( file_id) . ok ( ) . as_ref ( ) == Some ( & app_name) )
252+ {
253+ do_parse_one ( db, config, file_id, module_name. as_str ( ) , args) . unwrap ( )
254+ } else {
255+ None
256+ }
257+ } ,
258+ )
259+ . flatten ( )
260+ . collect ( ) )
115261}
116262
117- impl Ssr {
118- pub fn is_format_normal ( & self ) -> bool {
119- self . format . is_none ( )
263+ fn do_parse_one (
264+ db : & Analysis ,
265+ config : & DiagnosticsConfig ,
266+ file_id : FileId ,
267+ name : & str ,
268+ args : & Ssr ,
269+ ) -> Result < Option < ( String , FileId , Vec < diagnostics:: Diagnostic > ) > > {
270+ if !args. include_tests && db. is_test_suite_or_test_helper ( file_id) ?. unwrap_or ( false ) {
271+ return Ok ( None ) ;
272+ }
273+
274+ // Run only the SSR lint configured in lints_from_config
275+ let diagnostics = db. with_db ( |database| {
276+ let sema = Semantic :: new ( database) ;
277+ let mut diags = Vec :: new ( ) ;
278+ config
279+ . lints_from_config
280+ . get_diagnostics ( & mut diags, & sema, file_id) ;
281+ sema. parse ( file_id) ;
282+ diags
283+ } ) ?;
284+
285+ if !diagnostics. is_empty ( ) {
286+ let res = ( name. to_string ( ) , file_id, diagnostics) ;
287+ Ok ( Some ( res) )
288+ } else {
289+ Ok ( None )
120290 }
121291}
292+
293+ fn print_diagnostic (
294+ diag : & diagnostics:: Diagnostic ,
295+ analysis : & Analysis ,
296+ file_id : FileId ,
297+ use_cli_severity : bool ,
298+ cli : & mut dyn Cli ,
299+ ) -> Result < ( ) , anyhow:: Error > {
300+ let line_index = analysis. line_index ( file_id) ?;
301+ writeln ! ( cli, " {}" , diag. print( & line_index, use_cli_severity) ) ?;
302+ Ok ( ( ) )
303+ }
304+
305+ fn print_diagnostic_json (
306+ diagnostic : & diagnostics:: Diagnostic ,
307+ analysis : & Analysis ,
308+ file_id : FileId ,
309+ path : & Path ,
310+ use_cli_severity : bool ,
311+ cli : & mut dyn Cli ,
312+ ) -> Result < ( ) , anyhow:: Error > {
313+ let line_index = analysis. line_index ( file_id) ?;
314+ let converted_diagnostic =
315+ convert:: ide_to_arc_diagnostic ( & line_index, path, diagnostic, use_cli_severity) ;
316+ writeln ! (
317+ cli,
318+ "{}" ,
319+ serde_json:: to_string( & converted_diagnostic) . unwrap_or_else( |err| panic!(
320+ "print_diagnostics_json failed for '{converted_diagnostic:?}': {err}"
321+ ) )
322+ ) ?;
323+ Ok ( ( ) )
324+ }
0 commit comments