feat(oxc_language_server): Support nested configs#9724
feat(oxc_language_server): Support nested configs#9724nrayburn-tech wants to merge 1 commit intooxc-project:mainfrom
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. |
CodSpeed Performance ReportMerging #9724 will create unknown performance changesComparing Summary
Benchmarks breakdown
|
| diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>, | ||
| options: Mutex<Options>, | ||
| gitignore_glob: Mutex<Vec<Gitignore>>, | ||
| nested_configs: RwLock<FxHashMap<PathBuf, ConfigStore>>, |
There was a problem hiding this comment.
Store a map of directory to config for all nested configs in the workspace.
| run: Run, | ||
| enable: bool, | ||
| config_path: String, | ||
| use_nested_configs: bool, |
There was a problem hiding this comment.
Allow opting into nested configs. If false, there shouldn't be any behavior change.
| async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) { | ||
| async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { | ||
| debug!("watched file did change"); | ||
| if self.options.lock().await.use_nested_configs { |
There was a problem hiding this comment.
If using nested configs, check the watched files to determine if they are .oxlintrc.json files (which are the only nested config files that are supported to my knowledge).
This just keeps the nested config map up to date with any workspace changes.
| ); | ||
| } | ||
|
|
||
| if self.options.lock().await.use_nested_configs { |
There was a problem hiding this comment.
If using nested configs, use Linter::new_with_nested_configs and pass in the nested config files. Otherwise, should be no behavior change. However, I did do some refactoring here to make this easier for me to read through.
| workspace.createFileSystemWatcher(oxlintConfigFileGlob), | ||
| ...workspaceConfigPatterns.map((workspaceConfigPattern) => { | ||
| return workspace.createFileSystemWatcher(workspaceConfigPattern); | ||
| }), |
There was a problem hiding this comment.
Watch any .oxlintrc.json file in the workspace.
Watch the file that the user has configured within their configuration.
This stops watching all variations of oxlint config files that were previously watched. Which, I think is safe.
| changes: initialConfigFiles.map(file => { | ||
| return { uri: file.toString(), type: FileChangeType.Created }; | ||
| }), | ||
| }); |
There was a problem hiding this comment.
On startup, send all found config files to the language server as "created", so that they get added to the nested config map.
| diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>, | ||
| options: Mutex<Options>, | ||
| gitignore_glob: Mutex<Vec<Gitignore>>, | ||
| nested_configs: RwLock<FxHashMap<PathBuf, ConfigStore>>, |
There was a problem hiding this comment.
I don't know if this is the correct type. RwLock<FxHashMap<PathBuf, ConfigStore>>. Maybe something else should be used, but this is the only one I could figure out how to make work.
There was a problem hiding this comment.
Seems ok to me, but I'm not an expert in the async data structures area 😄 Might be worth looking at https://docs.rs/papaya/latest/papaya/, as I know we have been using that recently?
| let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
| for (key, value) in existing_nested_configs { | ||
| config_files.insert(key.clone(), value.clone()); | ||
| } |
There was a problem hiding this comment.
I am copying the map here, so I can mutate it later, before setting the value back into self.nested_configs. There might be a better way to do this that I don't know about.
There was a problem hiding this comment.
Might be worth looking at https://doc.rust-lang.org/std/mem/fn.take.html. It still requires allocating an empty hash map, but that is much cheaper compared to cloning a full hash map. Then after you've taken the hash map, you have full ownership over it and can insert into as needed.
| let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
| for (key, value) in existing_nested_configs { | ||
| config_files.insert(key.clone(), value.clone()); | ||
| } |
There was a problem hiding this comment.
Copying the map so that I can pass it into Linter::new_with_nested_configs. Might be a better way to do the copy that I'm not aware of.
There was a problem hiding this comment.
maybe std::mem::take? https://doc.rust-lang.org/std/mem/fn.take.html but not sure if that works in async context like this
| } | ||
|
|
||
| #[derive(Debug)] | ||
| #[derive(Debug, Clone)] |
There was a problem hiding this comment.
Needed so that I can clone my map that contains this struct.
|
|
||
| /// Resolves a lint configuration for a given file, by applying overrides based on the file's path. | ||
| #[derive(Debug)] | ||
| #[derive(Debug, Clone)] |
There was a problem hiding this comment.
Needed so that I can clone my map that contains this struct.
|
@camchenry can you review this to make sure the logic lines up with how the CLI works? (And any assistance with the code that looks odd would be helpful to, like if I should be doing something different with the maps instead of cloning.) |
|
@Sysix can you make sure this functionality makes sense to you from the language server perspective? (And any feedback with how the code is actually written would be appreciated too.) |
camchenry
left a comment
There was a problem hiding this comment.
I think this all looks right so far. I guess one advantage of having the actual config resolution in just oxc_linter is that it should "just work". As long as the config files are found correctly and passed in, the config files should just get resolved as expected, same as in the CLI (hopefully).
| let mut config_files = FxHashMap::default(); | ||
| let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
| for (key, value) in existing_nested_configs { | ||
| config_files.insert(key.clone(), value.clone()); |
There was a problem hiding this comment.
instead of inserting one-by-one, maybe we can try extending the hash map all at once? this should be more efficient because it allocates the needed memory upfront: https://doc.rust-lang.org/nightly/std/collections/struct.HashMap.html#impl-Extend%3C(K,+V)%3E-for-HashMap%3CK,+V,+S%3E
| let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
| for (key, value) in existing_nested_configs { | ||
| config_files.insert(key.clone(), value.clone()); | ||
| } |
There was a problem hiding this comment.
Might be worth looking at https://doc.rust-lang.org/std/mem/fn.take.html. It still requires allocating an empty hash map, but that is much cheaper compared to cloning a full hash map. Then after you've taken the hash map, you have full ownership over it and can insert into as needed.
| if x.uri.to_file_path().unwrap().file_name().unwrap() != OXC_CONFIG_FILE { | ||
| return; | ||
| } | ||
| // spellchecker:off -- "typ" is accurate | ||
| if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | ||
| // spellchecker:on | ||
| let file_path = x.uri.to_file_path().unwrap(); |
There was a problem hiding this comment.
we could avoid a duplicate unwrap here and some work by saving to_file_path():
| if x.uri.to_file_path().unwrap().file_name().unwrap() != OXC_CONFIG_FILE { | |
| return; | |
| } | |
| // spellchecker:off -- "typ" is accurate | |
| if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | |
| // spellchecker:on | |
| let file_path = x.uri.to_file_path().unwrap(); | |
| let Some(file_path) = x.uri.to_file_path() else { | |
| return; | |
| }; | |
| if file_path.file_name().unwrap() != OXC_CONFIG_FILE { | |
| return; | |
| } | |
| // spellchecker:off -- "typ" is accurate | |
| if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | |
| // spellchecker:on |
| let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
| for (key, value) in existing_nested_configs { | ||
| config_files.insert(key.clone(), value.clone()); | ||
| } |
There was a problem hiding this comment.
maybe std::mem::take? https://doc.rust-lang.org/std/mem/fn.take.html but not sure if that works in async context like this
Just putting this up to have the idea validated before I clean it up and separate it out into different PRs.
Left comments inline to sort of communicate what I'm doing or why. I don't do much in Rust, so I think it might be helpful to communicate what I'm trying to do in case there are better alternatives.
.unwrapthat I want to review and fix.