From fe48bfae0c2aa7df47f356324acd1da560c56917 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY Date: Mon, 8 Jan 2024 11:38:25 +0800 Subject: [PATCH] feat(lsp): support vue, astro and svelte (#1923) ![image](https://github.com/oxc-project/oxc/assets/17974631/7d49b2a7-b587-45a9-8736-875a2e60f06e) Closed https://github.com/oxc-project/oxc/issues/1915 --- crates/oxc_language_server/src/linter.rs | 175 ++++++++++-------- crates/oxc_language_server/src/main.rs | 4 +- crates/oxc_linter/src/partial_loader/astro.rs | 2 + crates/oxc_linter/src/partial_loader/mod.rs | 7 +- .../oxc_linter/src/partial_loader/svelte.rs | 2 +- crates/oxc_linter/src/partial_loader/vue.rs | 2 +- crates/oxc_linter/src/service.rs | 4 +- editors/vscode/client/extension.ts | 1 + editors/vscode/package.json | 5 +- 9 files changed, 114 insertions(+), 88 deletions(-) diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index d314794e3..dc4c8f479 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -10,7 +10,13 @@ use crate::options::LintOptions; use miette::NamedSource; use oxc_allocator::Allocator; use oxc_diagnostics::{miette, Error, Severity}; -use oxc_linter::{partial_loader::LINT_PARTIAL_LOADER_EXT, LintContext, LintSettings, Linter}; +use oxc_linter::{ + partial_loader::{ + AstroPartialLoader, JavaScriptSource, SveltePartialLoader, VuePartialLoader, + LINT_PARTIAL_LOADER_EXT, + }, + LintContext, LintSettings, Linter, +}; use oxc_linter_plugin::{make_relative_path_parts, LinterPlugin}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; @@ -37,14 +43,23 @@ struct LabeledSpanWithPosition { } impl ErrorWithPosition { - pub fn new(error: Error, text: &str, fixed_content: Option) -> Self { + pub fn new( + error: Error, + text: &str, + fixed_content: Option, + start: usize, + ) -> Self { let labels = error.labels().map_or(vec![], Iterator::collect); let labels_with_pos: Vec = labels .iter() .map(|labeled_span| LabeledSpanWithPosition { - start_pos: offset_to_position(labeled_span.offset(), text).unwrap_or_default(), - end_pos: offset_to_position(labeled_span.offset() + labeled_span.len(), text) + start_pos: offset_to_position(labeled_span.offset() + start, text) .unwrap_or_default(), + end_pos: offset_to_position( + labeled_span.offset() + start + labeled_span.len(), + text, + ) + .unwrap_or_default(), message: labeled_span.label().map(ToString::to_string), }) .collect(); @@ -161,8 +176,9 @@ impl IsolatedLintHandler { path: &Path, content: Option, ) -> Option> { + debug!("run single {path:?}"); if Self::is_wanted_ext(path) { - Some(Self::lint_path(&self.linter, path, Arc::clone(&self.plugin), content).map_or( + Some(Self::lint_path(&self.linter, path, &Arc::clone(&self.plugin), content).map_or( vec![], |(p, errors)| { let mut diagnostics: Vec = @@ -211,7 +227,7 @@ impl IsolatedLintHandler { } fn is_wanted_ext(path: &Path) -> bool { - let extensions = get_extensions(); + let extensions = get_valid_extensions(); path.extension().map_or(false, |ext| extensions.contains(&ext.to_string_lossy().as_ref())) } @@ -237,115 +253,117 @@ impl IsolatedLintHandler { } fn may_need_extract_js_content<'a>( - _source_text: &'a str, - _ext: &str, - ) -> Option<(&'a str, SourceType)> { - None - // match ext { - // "vue" => PartialLoader::Vue.build(source_text), - // "astro" => PartialLoader::Astro.build(source_text)), - // _ => None, - // } + source_text: &'a str, + ext: &str, + ) -> Option>> { + match ext { + "vue" => Some(VuePartialLoader::new(source_text).parse()), + "astro" => Some(AstroPartialLoader::new(source_text).parse()), + "svelte" => Some(SveltePartialLoader::new(source_text).parse()), + _ => None, + } } fn lint_path( linter: &Linter, path: &Path, - plugin: Plugin, + plugin: &Plugin, source_text: Option, ) -> Option<(PathBuf, Vec)> { let ext = path.extension().and_then(std::ffi::OsStr::to_str)?; - let (source_type, source_text) = Self::get_source_type_and_text(path, source_text, ext)?; - let (source_text, source_type) = Self::may_need_extract_js_content(&source_text, ext) - .unwrap_or((&source_text, source_type)); + let (source_type, original_source_text) = + Self::get_source_type_and_text(path, source_text, ext)?; + let javascript_sources = Self::may_need_extract_js_content(&original_source_text, ext) + .unwrap_or_else(|| { + vec![JavaScriptSource { source_text: &original_source_text, source_type, start: 0 }] + }); debug!("lint {path:?}"); + let mut diagnostics = vec![]; + for source in javascript_sources { + let JavaScriptSource { source_text: javascript_source_text, source_type, start } = + source; + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, javascript_source_text, source_type) + .allow_return_outside_function(true) + .parse(); - let allocator = Allocator::default(); - let ret = Parser::new(&allocator, source_text, source_type) - .allow_return_outside_function(true) - .parse(); + if !ret.errors.is_empty() { + let reports = ret + .errors + .into_iter() + .map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None }) + .collect(); + return Some(Self::wrap_diagnostics(path, &original_source_text, reports, start)); + }; - if !ret.errors.is_empty() { - let reports = ret - .errors - .into_iter() - .map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None }) - .collect(); + let program = allocator.alloc(ret.program); + let semantic_ret = SemanticBuilder::new(javascript_source_text, source_type) + .with_trivias(ret.trivias) + .with_check_syntax_error(true) + .build(program); - return Some(Self::wrap_diagnostics(path, source_text, reports)); - }; + if !semantic_ret.errors.is_empty() { + let reports = semantic_ret + .errors + .into_iter() + .map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None }) + .collect(); + return Some(Self::wrap_diagnostics(path, &original_source_text, reports, start)); + }; - let program = allocator.alloc(ret.program); - let semantic_ret = SemanticBuilder::new(source_text, source_type) - .with_trivias(ret.trivias) - .with_check_syntax_error(true) - .build(program); - - if !semantic_ret.errors.is_empty() { - let reports = semantic_ret - .errors - .into_iter() - .map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None }) - .collect(); - return Some(Self::wrap_diagnostics(path, source_text, reports)); - }; - - let mut lint_ctx = LintContext::new( - path.to_path_buf().into_boxed_path(), - &Rc::new(semantic_ret.semantic), - LintSettings::default(), - ); - { - if let Ok(guard) = plugin.read() { - if let Some(plugin) = &*guard { - plugin - .lint_file(&mut lint_ctx, make_relative_path_parts(&path.into())) - .unwrap(); + let mut lint_ctx = LintContext::new( + path.to_path_buf().into_boxed_path(), + &Rc::new(semantic_ret.semantic), + LintSettings::default(), + ); + { + if let Ok(guard) = plugin.read() { + if let Some(plugin) = &*guard { + plugin + .lint_file(&mut lint_ctx, make_relative_path_parts(&path.into())) + .unwrap(); + } } } - } - drop(plugin); // explicitly drop plugin so that we consume the plugin in this function's body + let result = linter.run(lint_ctx); - let result = linter.run(lint_ctx); - - if result.is_empty() { - return None; - } - - if linter.options().fix { let reports = result .into_iter() .map(|msg| { let fixed_content = msg.fix.map(|f| FixedContent { code: f.content.to_string(), range: Range { - start: offset_to_position(f.span.start as usize, source_text) - .unwrap_or_default(), - end: offset_to_position(f.span.end as usize, source_text) - .unwrap_or_default(), + start: offset_to_position( + f.span.start as usize + start, + javascript_source_text, + ) + .unwrap_or_default(), + end: offset_to_position( + f.span.end as usize + start, + javascript_source_text, + ) + .unwrap_or_default(), }, }); ErrorReport { error: msg.error, fixed_content } }) .collect::>(); - - return Some(Self::wrap_diagnostics(path, source_text, reports)); + let (_, errors_with_position) = + Self::wrap_diagnostics(path, &original_source_text, reports, start); + diagnostics.extend(errors_with_position); } - let errors = result - .into_iter() - .map(|diagnostic| ErrorReport { error: diagnostic.error, fixed_content: None }) - .collect(); - Some(Self::wrap_diagnostics(path, source_text, errors)) + Some((path.to_path_buf(), diagnostics)) } fn wrap_diagnostics( path: &Path, source_text: &str, reports: Vec, + start: usize, ) -> (PathBuf, Vec) { let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned())); let diagnostics = reports @@ -355,6 +373,7 @@ impl IsolatedLintHandler { report.error.with_source_code(Arc::clone(&source)), source_text, report.fixed_content, + start, ) }) .collect(); @@ -362,7 +381,7 @@ impl IsolatedLintHandler { } } -fn get_extensions() -> Vec<&'static str> { +fn get_valid_extensions() -> Vec<&'static str> { VALID_EXTENSIONS .iter() .chain(LINT_PARTIAL_LOADER_EXT.iter()) diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index 2d92a0773..08d90d756 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -323,7 +323,7 @@ impl Backend { .await; } - async fn handle_file_update(&self, uri: Url, content: Option, _version: Option) { + async fn handle_file_update(&self, uri: Url, content: Option, version: Option) { if let Some(Some(root_uri)) = self.root_uri.get() { self.server_linter.make_plugin(root_uri); if let Some(diagnostics) = self.server_linter.run_single(root_uri, &uri, content) { @@ -331,7 +331,7 @@ impl Backend { .publish_diagnostics( uri.clone(), diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(), - None, + version, ) .await; diff --git a/crates/oxc_linter/src/partial_loader/astro.rs b/crates/oxc_linter/src/partial_loader/astro.rs index 8bc28c537..2fbb6b5ee 100644 --- a/crates/oxc_linter/src/partial_loader/astro.rs +++ b/crates/oxc_linter/src/partial_loader/astro.rs @@ -43,6 +43,7 @@ impl<'a> AstroPartialLoader<'a> { Some(JavaScriptSource::new( js_code, SourceType::default().with_typescript(true).with_module(true), + start as usize, )) } @@ -81,6 +82,7 @@ impl<'a> AstroPartialLoader<'a> { results.push(JavaScriptSource::new( &self.source_text[js_start..js_end], SourceType::default().with_typescript(true).with_module(true), + js_start, )); } results diff --git a/crates/oxc_linter/src/partial_loader/mod.rs b/crates/oxc_linter/src/partial_loader/mod.rs index 64c783c0f..fdd7b9e5a 100644 --- a/crates/oxc_linter/src/partial_loader/mod.rs +++ b/crates/oxc_linter/src/partial_loader/mod.rs @@ -15,11 +15,14 @@ pub const LINT_PARTIAL_LOADER_EXT: &[&str] = &["vue", "astro", "svelte"]; pub struct JavaScriptSource<'a> { pub source_text: &'a str, pub source_type: SourceType, + /// The javascript source could be embedded in some file, + /// use `start` to record start offset of js block in the original file. + pub start: usize, } impl<'a> JavaScriptSource<'a> { - pub fn new(source_text: &'a str, source_type: SourceType) -> Self { - Self { source_text, source_type } + pub fn new(source_text: &'a str, source_type: SourceType, start: usize) -> Self { + Self { source_text, source_type, start } } } diff --git a/crates/oxc_linter/src/partial_loader/svelte.rs b/crates/oxc_linter/src/partial_loader/svelte.rs index efb866f90..016416aa9 100644 --- a/crates/oxc_linter/src/partial_loader/svelte.rs +++ b/crates/oxc_linter/src/partial_loader/svelte.rs @@ -43,6 +43,6 @@ impl<'a> SveltePartialLoader<'a> { let source_text = &self.source_text[js_start..js_end]; let source_type = SourceType::default().with_module(true).with_typescript(is_ts); - Some(JavaScriptSource::new(source_text, source_type)) + Some(JavaScriptSource::new(source_text, source_type, js_start)) } } diff --git a/crates/oxc_linter/src/partial_loader/vue.rs b/crates/oxc_linter/src/partial_loader/vue.rs index 7aff48eb9..2405d23b0 100644 --- a/crates/oxc_linter/src/partial_loader/vue.rs +++ b/crates/oxc_linter/src/partial_loader/vue.rs @@ -55,7 +55,7 @@ impl<'a> VuePartialLoader<'a> { let source_text = &self.source_text[js_start..js_end]; let source_type = SourceType::default().with_module(true).with_typescript(is_ts).with_jsx(is_jsx); - Some(JavaScriptSource::new(source_text, source_type)) + Some(JavaScriptSource::new(source_text, source_type, js_start)) } } diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 17543f0d1..adf8f7b5a 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -176,13 +176,13 @@ impl Runtime { }; let sources = PartialLoader::parse(ext, &source_text) - .unwrap_or_else(|| vec![JavaScriptSource::new(&source_text, source_type)]); + .unwrap_or_else(|| vec![JavaScriptSource::new(&source_text, source_type, 0)]); if sources.is_empty() { return; } - for JavaScriptSource { source_text, source_type } in sources { + for JavaScriptSource { source_text, source_type, .. } in sources { let allocator = Allocator::default(); let mut messages = self.process_source(path, &allocator, source_text, source_type, true, tx_error); diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 17e55d94c..d3f6d1b9f 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -127,6 +127,7 @@ export async function activate(context: ExtensionContext) { "typescriptreact", "javascriptreact", "vue", + "svelte", ].map((lang) => ({ language: lang, scheme: "file", diff --git a/editors/vscode/package.json b/editors/vscode/package.json index c94e29fc0..151d9e576 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -28,11 +28,12 @@ "url": "https://github.com/sponsors/boshen" }, "activationEvents": [ - "onStartupFinished", "onLanguage:javascript", "onLanguage:javascriptreact", "onLanguage:typescript", - "onLanguage:typescriptreact" + "onLanguage:typescriptreact", + "onLanguage:vue", + "onLanguage:svelte" ], "main": "./out/main.js", "contributes": {