1+ use std:: path:: { Path , PathBuf } ;
12use std:: sync:: Arc ;
23
4+ use globset:: Glob ;
5+ use ignore:: gitignore:: Gitignore ;
6+ use log:: warn;
7+ use rustc_hash:: { FxBuildHasher , FxHashMap } ;
38use tower_lsp_server:: lsp_types:: Uri ;
49
5- use oxc_linter:: Linter ;
10+ use oxc_linter:: { ConfigStore , ConfigStoreBuilder , LintOptions , Linter , Oxlintrc } ;
11+ use tower_lsp_server:: UriExt ;
612
7- use crate :: linter:: error_with_position:: DiagnosticReport ;
8- use crate :: linter:: isolated_lint_handler:: IsolatedLintHandler ;
13+ use crate :: linter:: {
14+ error_with_position:: DiagnosticReport ,
15+ isolated_lint_handler:: { IsolatedLintHandler , IsolatedLintHandlerOptions } ,
16+ } ;
17+ use crate :: { ConcurrentHashMap , Options } ;
918
10- use super :: isolated_lint_handler :: IsolatedLintHandlerOptions ;
19+ use super :: config_walker :: ConfigWalker ;
1120
1221#[ derive( Clone ) ]
1322pub struct ServerLinter {
1423 isolated_linter : Arc < IsolatedLintHandler > ,
1524}
1625
1726impl ServerLinter {
18- pub fn new_with_linter ( linter : Linter , options : IsolatedLintHandlerOptions ) -> Self {
27+ /// Searches inside root_uri recursively for the default oxlint config files
28+ /// and insert them inside the nested configuration
29+ pub fn create_nested_configs (
30+ root_uri : & Uri ,
31+ options : & Options ,
32+ ) -> ConcurrentHashMap < PathBuf , ConfigStore > {
33+ // nested config is disabled, no need to search for configs
34+ if !options. use_nested_configs ( ) {
35+ return ConcurrentHashMap :: default ( ) ;
36+ }
37+
38+ let root_path = root_uri. to_file_path ( ) . expect ( "Failed to convert URI to file path" ) ;
39+
40+ let paths = ConfigWalker :: new ( & root_path) . paths ( ) ;
41+ let nested_configs =
42+ ConcurrentHashMap :: with_capacity_and_hasher ( paths. capacity ( ) , FxBuildHasher ) ;
43+
44+ for path in paths {
45+ let file_path = Path :: new ( & path) ;
46+ let Some ( dir_path) = file_path. parent ( ) else {
47+ continue ;
48+ } ;
49+
50+ let Ok ( oxlintrc) = Oxlintrc :: from_file ( file_path) else {
51+ warn ! ( "Skipping invalid config file: {}" , file_path. display( ) ) ;
52+ continue ;
53+ } ;
54+ let Ok ( config_store_builder) = ConfigStoreBuilder :: from_oxlintrc ( false , oxlintrc)
55+ else {
56+ warn ! ( "Skipping config (builder failed): {}" , file_path. display( ) ) ;
57+ continue ;
58+ } ;
59+ let Ok ( config_store) = config_store_builder. build ( ) else {
60+ warn ! ( "Skipping config (builder failed): {}" , file_path. display( ) ) ;
61+ continue ;
62+ } ;
63+
64+ nested_configs. pin ( ) . insert ( dir_path. to_path_buf ( ) , config_store) ;
65+ }
66+
67+ nested_configs
68+ }
69+
70+ pub fn create_ignore_glob ( root_uri : & Uri , oxlintrc : & Oxlintrc ) -> Vec < Gitignore > {
71+ let mut builder = globset:: GlobSetBuilder :: new ( ) ;
72+ // Collecting all ignore files
73+ builder. add ( Glob :: new ( "**/.eslintignore" ) . unwrap ( ) ) ;
74+ builder. add ( Glob :: new ( "**/.gitignore" ) . unwrap ( ) ) ;
75+
76+ let ignore_file_glob_set = builder. build ( ) . unwrap ( ) ;
77+
78+ let walk = ignore:: WalkBuilder :: new ( root_uri. to_file_path ( ) . unwrap ( ) )
79+ . ignore ( true )
80+ . hidden ( false )
81+ . git_global ( false )
82+ . build ( )
83+ . flatten ( ) ;
84+
85+ let mut gitignore_globs = vec ! [ ] ;
86+ for entry in walk {
87+ let ignore_file_path = entry. path ( ) ;
88+ if !ignore_file_glob_set. is_match ( ignore_file_path) {
89+ continue ;
90+ }
91+
92+ if let Some ( ignore_file_dir) = ignore_file_path. parent ( ) {
93+ let mut builder = ignore:: gitignore:: GitignoreBuilder :: new ( ignore_file_dir) ;
94+ builder. add ( ignore_file_path) ;
95+ if let Ok ( gitignore) = builder. build ( ) {
96+ gitignore_globs. push ( gitignore) ;
97+ }
98+ }
99+ }
100+
101+ if oxlintrc. ignore_patterns . is_empty ( ) {
102+ return gitignore_globs;
103+ }
104+
105+ let Some ( oxlintrc_dir) = oxlintrc. path . parent ( ) else {
106+ warn ! ( "Oxlintrc path has no parent, skipping inline ignore patterns" ) ;
107+ return gitignore_globs;
108+ } ;
109+
110+ let mut builder = ignore:: gitignore:: GitignoreBuilder :: new ( oxlintrc_dir) ;
111+ for entry in & oxlintrc. ignore_patterns {
112+ builder. add_line ( None , entry) . expect ( "Failed to add ignore line" ) ;
113+ }
114+ gitignore_globs. push ( builder. build ( ) . unwrap ( ) ) ;
115+ gitignore_globs
116+ }
117+
118+ pub fn create_server_linter (
119+ root_uri : & Uri ,
120+ options : & Options ,
121+ nested_configs : & ConcurrentHashMap < PathBuf , ConfigStore > ,
122+ ) -> ( Self , Oxlintrc ) {
123+ let root_path = root_uri. to_file_path ( ) . unwrap ( ) ;
124+ let relative_config_path = options. config_path . clone ( ) ;
125+ let oxlintrc = if relative_config_path. is_some ( ) {
126+ let config = root_path. join ( relative_config_path. unwrap ( ) ) ;
127+ if config. try_exists ( ) . expect ( "Could not get fs metadata for config" ) {
128+ if let Ok ( oxlintrc) = Oxlintrc :: from_file ( & config) {
129+ oxlintrc
130+ } else {
131+ warn ! ( "Failed to initialize oxlintrc config: {}" , config. to_string_lossy( ) ) ;
132+ Oxlintrc :: default ( )
133+ }
134+ } else {
135+ warn ! (
136+ "Config file not found: {}, fallback to default config" ,
137+ config. to_string_lossy( )
138+ ) ;
139+ Oxlintrc :: default ( )
140+ }
141+ } else {
142+ Oxlintrc :: default ( )
143+ } ;
144+
145+ // clone because we are returning it for ignore builder
146+ let config_builder =
147+ ConfigStoreBuilder :: from_oxlintrc ( false , oxlintrc. clone ( ) ) . unwrap_or_default ( ) ;
148+
149+ // TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality.
150+ let use_nested_config = options. use_nested_configs ( ) ;
151+
152+ let use_cross_module = if use_nested_config {
153+ nested_configs. pin ( ) . values ( ) . any ( |config| config. plugins ( ) . has_import ( ) )
154+ } else {
155+ config_builder. plugins ( ) . has_import ( )
156+ } ;
157+
158+ let config_store = config_builder. build ( ) . expect ( "Failed to build config store" ) ;
159+
160+ let lint_options = LintOptions { fix : options. fix_kind ( ) , ..Default :: default ( ) } ;
161+
162+ let linter = if use_nested_config {
163+ let nested_configs = nested_configs. pin ( ) ;
164+ let nested_configs_copy: FxHashMap < PathBuf , ConfigStore > = nested_configs
165+ . iter ( )
166+ . map ( |( key, value) | ( key. clone ( ) , value. clone ( ) ) )
167+ . collect :: < FxHashMap < _ , _ > > ( ) ;
168+
169+ Linter :: new_with_nested_configs ( lint_options, config_store, nested_configs_copy)
170+ } else {
171+ Linter :: new ( lint_options, config_store)
172+ } ;
173+
174+ let server_linter = ServerLinter :: new_with_linter (
175+ linter,
176+ IsolatedLintHandlerOptions { use_cross_module, root_path : root_path. to_path_buf ( ) } ,
177+ ) ;
178+
179+ ( server_linter, oxlintrc)
180+ }
181+
182+ fn new_with_linter ( linter : Linter , options : IsolatedLintHandlerOptions ) -> Self {
19183 let isolated_linter = Arc :: new ( IsolatedLintHandler :: new ( linter, options) ) ;
20184
21185 Self { isolated_linter }
@@ -28,7 +192,46 @@ impl ServerLinter {
28192
29193#[ cfg( test) ]
30194mod test {
31- use crate :: tester:: Tester ;
195+ use std:: { path:: PathBuf , str:: FromStr } ;
196+
197+ use rustc_hash:: FxHashMap ;
198+ use tower_lsp_server:: lsp_types:: Uri ;
199+
200+ use crate :: {
201+ Options ,
202+ linter:: server_linter:: ServerLinter ,
203+ tester:: { Tester , get_file_uri} ,
204+ } ;
205+
206+ #[ test]
207+ fn test_create_nested_configs_with_disabled_nested_configs ( ) {
208+ let mut flags = FxHashMap :: default ( ) ;
209+ flags. insert ( "disable_nested_configs" . to_string ( ) , "true" . to_string ( ) ) ;
210+
211+ let configs = ServerLinter :: create_nested_configs (
212+ & Uri :: from_str ( "file:///root/" ) . unwrap ( ) ,
213+ & Options { flags, ..Options :: default ( ) } ,
214+ ) ;
215+
216+ assert ! ( configs. is_empty( ) ) ;
217+ }
218+
219+ #[ test]
220+ fn test_create_nested_configs ( ) {
221+ let configs = ServerLinter :: create_nested_configs (
222+ & get_file_uri ( "fixtures/linter/init_nested_configs" ) ,
223+ & Options :: default ( ) ,
224+ ) ;
225+ let configs = configs. pin ( ) ;
226+ let mut configs_dirs = configs. keys ( ) . collect :: < Vec < & PathBuf > > ( ) ;
227+ // sorting the key because for consistent tests results
228+ configs_dirs. sort ( ) ;
229+
230+ assert ! ( configs_dirs. len( ) == 3 ) ;
231+ assert ! ( configs_dirs[ 2 ] . ends_with( "deep2" ) ) ;
232+ assert ! ( configs_dirs[ 1 ] . ends_with( "deep1" ) ) ;
233+ assert ! ( configs_dirs[ 0 ] . ends_with( "init_nested_configs" ) ) ;
234+ }
32235
33236 #[ test]
34237 fn test_no_errors ( ) {
0 commit comments