@@ -9,10 +9,11 @@ use cow_utils::CowUtils;
9
9
use ignore:: { gitignore:: Gitignore , overrides:: OverrideBuilder } ;
10
10
use oxc_diagnostics:: { DiagnosticService , GraphicalReportHandler } ;
11
11
use oxc_linter:: {
12
- loader:: LINT_PARTIAL_LOADER_EXT , AllowWarnDeny , ConfigStoreBuilder , InvalidFilterKind ,
13
- LintFilter , LintOptions , LintService , LintServiceOptions , Linter , Oxlintrc ,
12
+ loader:: LINT_PARTIAL_LOADER_EXT , AllowWarnDeny , ConfigStore , ConfigStoreBuilder ,
13
+ InvalidFilterKind , LintFilter , LintOptions , LintService , LintServiceOptions , Linter , Oxlintrc ,
14
14
} ;
15
15
use oxc_span:: VALID_EXTENSIONS ;
16
+ use rustc_hash:: { FxHashMap , FxHashSet } ;
16
17
use serde_json:: Value ;
17
18
18
19
use crate :: {
@@ -55,6 +56,7 @@ impl Runner for LintRunner {
55
56
fix_options,
56
57
enable_plugins,
57
58
misc_options,
59
+ experimental_nested_config,
58
60
..
59
61
} = self . options ;
60
62
@@ -168,6 +170,56 @@ impl Runner for LintRunner {
168
170
169
171
let number_of_files = paths. len ( ) ;
170
172
173
+ // TODO(perf): benchmark whether or not it is worth it to store the configurations on a
174
+ // per-file or per-directory basis, to avoid calling `.parent()` on every path.
175
+ let mut nested_oxlintrc = FxHashMap :: < & Path , Oxlintrc > :: default ( ) ;
176
+ let mut nested_configs = FxHashMap :: < PathBuf , ConfigStore > :: default ( ) ;
177
+
178
+ if experimental_nested_config {
179
+ // get all of the unique directories among the paths to use for search for
180
+ // oxlint config files in those directories
181
+ // e.g. `/some/file.js` and `/some/other/file.js` would both result in `/some`
182
+ let mut directories = FxHashSet :: default ( ) ;
183
+ for path in & paths {
184
+ if let Some ( directory) = path. parent ( ) {
185
+ // TODO(perf): benchmark whether or not it is worth it to produce the config files here without
186
+ // iterating over the directories again. it might cause the cache for paths to be disrupted, but
187
+ // it is unclear whether or not that is a problem in practice.
188
+ directories. insert ( directory) ;
189
+ }
190
+ }
191
+ for directory in directories {
192
+ // TODO: how should we handle a nested config error?
193
+ if let Ok ( config) = Self :: find_oxlint_config_in_directory ( directory) {
194
+ nested_oxlintrc. insert ( directory, config) ;
195
+ }
196
+ }
197
+
198
+ // iterate over each config and build the ConfigStore
199
+ for ( dir, oxlintrc) in nested_oxlintrc {
200
+ // TODO(perf): figure out if we can avoid cloning `filter`
201
+ let builder =
202
+ ConfigStoreBuilder :: from_oxlintrc ( false , oxlintrc) . with_filters ( filter. clone ( ) ) ;
203
+ match builder. build ( ) {
204
+ Ok ( config) => nested_configs. insert ( dir. to_path_buf ( ) , config) ,
205
+ Err ( diagnostic) => {
206
+ let handler = GraphicalReportHandler :: new ( ) ;
207
+ let mut err = String :: new ( ) ;
208
+ handler. render_report ( & mut err, & diagnostic) . unwrap ( ) ;
209
+ stdout
210
+ . write_all (
211
+ format ! ( "Failed to parse configuration file.\n {err}\n " ) . as_bytes ( ) ,
212
+ )
213
+ . or_else ( Self :: check_for_writer_error)
214
+ . unwrap ( ) ;
215
+ stdout. flush ( ) . unwrap ( ) ;
216
+
217
+ return CliRunResult :: InvalidOptionConfig ;
218
+ }
219
+ } ;
220
+ }
221
+ }
222
+
171
223
enable_plugins. apply_overrides ( & mut oxlintrc. plugins ) ;
172
224
173
225
let oxlintrc_for_print = if misc_options. print_config || basic_options. init {
@@ -245,8 +297,11 @@ impl Runner for LintRunner {
245
297
}
246
298
} ;
247
299
248
- let linter =
249
- Linter :: new ( LintOptions :: default ( ) , lint_config) . with_fix ( fix_options. fix_kind ( ) ) ;
300
+ let linter = if experimental_nested_config {
301
+ Linter :: new_with_nested_configs ( LintOptions :: default ( ) , lint_config, nested_configs)
302
+ } else {
303
+ Linter :: new ( LintOptions :: default ( ) , lint_config) . with_fix ( fix_options. fix_kind ( ) )
304
+ } ;
250
305
251
306
let tsconfig = basic_options. tsconfig ;
252
307
if let Some ( path) = tsconfig. as_ref ( ) {
@@ -381,6 +436,24 @@ impl LintRunner {
381
436
Oxlintrc :: from_file ( & config_path) . or_else ( |_| Ok ( Oxlintrc :: default ( ) ) )
382
437
}
383
438
439
+ /// Looks in a directory for an oxlint config file, returns the oxlint config if it exists
440
+ /// and returns `Err` if none exists or the file is invalid. Does not apply the default
441
+ /// config file.
442
+ fn find_oxlint_config_in_directory ( dir : & Path ) -> Result < Oxlintrc , String > {
443
+ let possible_config_path = dir. join ( Self :: DEFAULT_OXLINTRC ) ;
444
+ if possible_config_path. is_file ( ) {
445
+ Oxlintrc :: from_file ( & possible_config_path) . map_err ( |e| {
446
+ let handler = GraphicalReportHandler :: new ( ) ;
447
+ let mut err = String :: new ( ) ;
448
+ handler. render_report ( & mut err, & e) . unwrap ( ) ;
449
+ err
450
+ } )
451
+ } else {
452
+ // TODO: Better error handling here.
453
+ Err ( "No oxlint config file found" . to_string ( ) )
454
+ }
455
+ }
456
+
384
457
fn check_for_writer_error ( error : std:: io:: Error ) -> Result < ( ) , std:: io:: Error > {
385
458
// Do not panic when the process is killed (e.g. piping into `less`).
386
459
if matches ! ( error. kind( ) , ErrorKind :: Interrupted | ErrorKind :: BrokenPipe ) {
@@ -866,4 +939,10 @@ mod test {
866
939
vec![ String :: from( "src/target" ) , String :: from( "!src/dist" ) , String :: from( "!!src/dist" ) ]
867
940
) ;
868
941
}
942
+
943
+ #[ test]
944
+ fn test_nested_config ( ) {
945
+ let args = & [ "--experimental-nested-config" ] ;
946
+ Tester :: new ( ) . with_cwd ( "fixtures/nested_config" . into ( ) ) . test_and_snapshot ( args) ;
947
+ }
869
948
}
0 commit comments