mirror of
https://github.com/danbulant/oxc
synced 2026-05-21 21:29:01 +00:00
feat(lsp): support vue, astro and svelte (#1923)
 Closed https://github.com/oxc-project/oxc/issues/1915
This commit is contained in:
parent
c6eb519417
commit
fe48bfae0c
9 changed files with 114 additions and 88 deletions
|
|
@ -10,7 +10,13 @@ use crate::options::LintOptions;
|
||||||
use miette::NamedSource;
|
use miette::NamedSource;
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_diagnostics::{miette, Error, Severity};
|
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_linter_plugin::{make_relative_path_parts, LinterPlugin};
|
||||||
use oxc_parser::Parser;
|
use oxc_parser::Parser;
|
||||||
use oxc_semantic::SemanticBuilder;
|
use oxc_semantic::SemanticBuilder;
|
||||||
|
|
@ -37,14 +43,23 @@ struct LabeledSpanWithPosition {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorWithPosition {
|
impl ErrorWithPosition {
|
||||||
pub fn new(error: Error, text: &str, fixed_content: Option<FixedContent>) -> Self {
|
pub fn new(
|
||||||
|
error: Error,
|
||||||
|
text: &str,
|
||||||
|
fixed_content: Option<FixedContent>,
|
||||||
|
start: usize,
|
||||||
|
) -> Self {
|
||||||
let labels = error.labels().map_or(vec![], Iterator::collect);
|
let labels = error.labels().map_or(vec![], Iterator::collect);
|
||||||
let labels_with_pos: Vec<LabeledSpanWithPosition> = labels
|
let labels_with_pos: Vec<LabeledSpanWithPosition> = labels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|labeled_span| LabeledSpanWithPosition {
|
.map(|labeled_span| LabeledSpanWithPosition {
|
||||||
start_pos: offset_to_position(labeled_span.offset(), text).unwrap_or_default(),
|
start_pos: offset_to_position(labeled_span.offset() + start, text)
|
||||||
end_pos: offset_to_position(labeled_span.offset() + labeled_span.len(), text)
|
|
||||||
.unwrap_or_default(),
|
.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),
|
message: labeled_span.label().map(ToString::to_string),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -161,8 +176,9 @@ impl IsolatedLintHandler {
|
||||||
path: &Path,
|
path: &Path,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
) -> Option<Vec<DiagnosticReport>> {
|
) -> Option<Vec<DiagnosticReport>> {
|
||||||
|
debug!("run single {path:?}");
|
||||||
if Self::is_wanted_ext(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![],
|
vec![],
|
||||||
|(p, errors)| {
|
|(p, errors)| {
|
||||||
let mut diagnostics: Vec<DiagnosticReport> =
|
let mut diagnostics: Vec<DiagnosticReport> =
|
||||||
|
|
@ -211,7 +227,7 @@ impl IsolatedLintHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_wanted_ext(path: &Path) -> bool {
|
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()))
|
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>(
|
fn may_need_extract_js_content<'a>(
|
||||||
_source_text: &'a str,
|
source_text: &'a str,
|
||||||
_ext: &str,
|
ext: &str,
|
||||||
) -> Option<(&'a str, SourceType)> {
|
) -> Option<Vec<JavaScriptSource<'a>>> {
|
||||||
None
|
match ext {
|
||||||
// match ext {
|
"vue" => Some(VuePartialLoader::new(source_text).parse()),
|
||||||
// "vue" => PartialLoader::Vue.build(source_text),
|
"astro" => Some(AstroPartialLoader::new(source_text).parse()),
|
||||||
// "astro" => PartialLoader::Astro.build(source_text)),
|
"svelte" => Some(SveltePartialLoader::new(source_text).parse()),
|
||||||
// _ => None,
|
_ => None,
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lint_path(
|
fn lint_path(
|
||||||
linter: &Linter,
|
linter: &Linter,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
plugin: Plugin,
|
plugin: &Plugin,
|
||||||
source_text: Option<String>,
|
source_text: Option<String>,
|
||||||
) -> Option<(PathBuf, Vec<ErrorWithPosition>)> {
|
) -> Option<(PathBuf, Vec<ErrorWithPosition>)> {
|
||||||
let ext = path.extension().and_then(std::ffi::OsStr::to_str)?;
|
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_type, original_source_text) =
|
||||||
let (source_text, source_type) = Self::may_need_extract_js_content(&source_text, ext)
|
Self::get_source_type_and_text(path, source_text, ext)?;
|
||||||
.unwrap_or((&source_text, source_type));
|
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:?}");
|
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();
|
if !ret.errors.is_empty() {
|
||||||
let ret = Parser::new(&allocator, source_text, source_type)
|
let reports = ret
|
||||||
.allow_return_outside_function(true)
|
.errors
|
||||||
.parse();
|
.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 program = allocator.alloc(ret.program);
|
||||||
let reports = ret
|
let semantic_ret = SemanticBuilder::new(javascript_source_text, source_type)
|
||||||
.errors
|
.with_trivias(ret.trivias)
|
||||||
.into_iter()
|
.with_check_syntax_error(true)
|
||||||
.map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None })
|
.build(program);
|
||||||
.collect();
|
|
||||||
|
|
||||||
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 mut lint_ctx = LintContext::new(
|
||||||
let semantic_ret = SemanticBuilder::new(source_text, source_type)
|
path.to_path_buf().into_boxed_path(),
|
||||||
.with_trivias(ret.trivias)
|
&Rc::new(semantic_ret.semantic),
|
||||||
.with_check_syntax_error(true)
|
LintSettings::default(),
|
||||||
.build(program);
|
);
|
||||||
|
{
|
||||||
if !semantic_ret.errors.is_empty() {
|
if let Ok(guard) = plugin.read() {
|
||||||
let reports = semantic_ret
|
if let Some(plugin) = &*guard {
|
||||||
.errors
|
plugin
|
||||||
.into_iter()
|
.lint_file(&mut lint_ctx, make_relative_path_parts(&path.into()))
|
||||||
.map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None })
|
.unwrap();
|
||||||
.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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
let reports = result
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|msg| {
|
.map(|msg| {
|
||||||
let fixed_content = msg.fix.map(|f| FixedContent {
|
let fixed_content = msg.fix.map(|f| FixedContent {
|
||||||
code: f.content.to_string(),
|
code: f.content.to_string(),
|
||||||
range: Range {
|
range: Range {
|
||||||
start: offset_to_position(f.span.start as usize, source_text)
|
start: offset_to_position(
|
||||||
.unwrap_or_default(),
|
f.span.start as usize + start,
|
||||||
end: offset_to_position(f.span.end as usize, source_text)
|
javascript_source_text,
|
||||||
.unwrap_or_default(),
|
)
|
||||||
|
.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 }
|
ErrorReport { error: msg.error, fixed_content }
|
||||||
})
|
})
|
||||||
.collect::<Vec<ErrorReport>>();
|
.collect::<Vec<ErrorReport>>();
|
||||||
|
let (_, errors_with_position) =
|
||||||
return Some(Self::wrap_diagnostics(path, source_text, reports));
|
Self::wrap_diagnostics(path, &original_source_text, reports, start);
|
||||||
|
diagnostics.extend(errors_with_position);
|
||||||
}
|
}
|
||||||
|
|
||||||
let errors = result
|
Some((path.to_path_buf(), diagnostics))
|
||||||
.into_iter()
|
|
||||||
.map(|diagnostic| ErrorReport { error: diagnostic.error, fixed_content: None })
|
|
||||||
.collect();
|
|
||||||
Some(Self::wrap_diagnostics(path, source_text, errors))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_diagnostics(
|
fn wrap_diagnostics(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
source_text: &str,
|
source_text: &str,
|
||||||
reports: Vec<ErrorReport>,
|
reports: Vec<ErrorReport>,
|
||||||
|
start: usize,
|
||||||
) -> (PathBuf, Vec<ErrorWithPosition>) {
|
) -> (PathBuf, Vec<ErrorWithPosition>) {
|
||||||
let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned()));
|
let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned()));
|
||||||
let diagnostics = reports
|
let diagnostics = reports
|
||||||
|
|
@ -355,6 +373,7 @@ impl IsolatedLintHandler {
|
||||||
report.error.with_source_code(Arc::clone(&source)),
|
report.error.with_source_code(Arc::clone(&source)),
|
||||||
source_text,
|
source_text,
|
||||||
report.fixed_content,
|
report.fixed_content,
|
||||||
|
start,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -362,7 +381,7 @@ impl IsolatedLintHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_extensions() -> Vec<&'static str> {
|
fn get_valid_extensions() -> Vec<&'static str> {
|
||||||
VALID_EXTENSIONS
|
VALID_EXTENSIONS
|
||||||
.iter()
|
.iter()
|
||||||
.chain(LINT_PARTIAL_LOADER_EXT.iter())
|
.chain(LINT_PARTIAL_LOADER_EXT.iter())
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ impl Backend {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_file_update(&self, uri: Url, content: Option<String>, _version: Option<i32>) {
|
async fn handle_file_update(&self, uri: Url, content: Option<String>, version: Option<i32>) {
|
||||||
if let Some(Some(root_uri)) = self.root_uri.get() {
|
if let Some(Some(root_uri)) = self.root_uri.get() {
|
||||||
self.server_linter.make_plugin(root_uri);
|
self.server_linter.make_plugin(root_uri);
|
||||||
if let Some(diagnostics) = self.server_linter.run_single(root_uri, &uri, content) {
|
if let Some(diagnostics) = self.server_linter.run_single(root_uri, &uri, content) {
|
||||||
|
|
@ -331,7 +331,7 @@ impl Backend {
|
||||||
.publish_diagnostics(
|
.publish_diagnostics(
|
||||||
uri.clone(),
|
uri.clone(),
|
||||||
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
|
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
|
||||||
None,
|
version,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ impl<'a> AstroPartialLoader<'a> {
|
||||||
Some(JavaScriptSource::new(
|
Some(JavaScriptSource::new(
|
||||||
js_code,
|
js_code,
|
||||||
SourceType::default().with_typescript(true).with_module(true),
|
SourceType::default().with_typescript(true).with_module(true),
|
||||||
|
start as usize,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +82,7 @@ impl<'a> AstroPartialLoader<'a> {
|
||||||
results.push(JavaScriptSource::new(
|
results.push(JavaScriptSource::new(
|
||||||
&self.source_text[js_start..js_end],
|
&self.source_text[js_start..js_end],
|
||||||
SourceType::default().with_typescript(true).with_module(true),
|
SourceType::default().with_typescript(true).with_module(true),
|
||||||
|
js_start,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
results
|
results
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,14 @@ pub const LINT_PARTIAL_LOADER_EXT: &[&str] = &["vue", "astro", "svelte"];
|
||||||
pub struct JavaScriptSource<'a> {
|
pub struct JavaScriptSource<'a> {
|
||||||
pub source_text: &'a str,
|
pub source_text: &'a str,
|
||||||
pub source_type: SourceType,
|
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> {
|
impl<'a> JavaScriptSource<'a> {
|
||||||
pub fn new(source_text: &'a str, source_type: SourceType) -> Self {
|
pub fn new(source_text: &'a str, source_type: SourceType, start: usize) -> Self {
|
||||||
Self { source_text, source_type }
|
Self { source_text, source_type, start }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,6 @@ impl<'a> SveltePartialLoader<'a> {
|
||||||
|
|
||||||
let source_text = &self.source_text[js_start..js_end];
|
let source_text = &self.source_text[js_start..js_end];
|
||||||
let source_type = SourceType::default().with_module(true).with_typescript(is_ts);
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ impl<'a> VuePartialLoader<'a> {
|
||||||
let source_text = &self.source_text[js_start..js_end];
|
let source_text = &self.source_text[js_start..js_end];
|
||||||
let source_type =
|
let source_type =
|
||||||
SourceType::default().with_module(true).with_typescript(is_ts).with_jsx(is_jsx);
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,13 +176,13 @@ impl Runtime {
|
||||||
};
|
};
|
||||||
|
|
||||||
let sources = PartialLoader::parse(ext, &source_text)
|
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() {
|
if sources.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for JavaScriptSource { source_text, source_type } in sources {
|
for JavaScriptSource { source_text, source_type, .. } in sources {
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let mut messages =
|
let mut messages =
|
||||||
self.process_source(path, &allocator, source_text, source_type, true, tx_error);
|
self.process_source(path, &allocator, source_text, source_type, true, tx_error);
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ export async function activate(context: ExtensionContext) {
|
||||||
"typescriptreact",
|
"typescriptreact",
|
||||||
"javascriptreact",
|
"javascriptreact",
|
||||||
"vue",
|
"vue",
|
||||||
|
"svelte",
|
||||||
].map((lang) => ({
|
].map((lang) => ({
|
||||||
language: lang,
|
language: lang,
|
||||||
scheme: "file",
|
scheme: "file",
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,12 @@
|
||||||
"url": "https://github.com/sponsors/boshen"
|
"url": "https://github.com/sponsors/boshen"
|
||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onStartupFinished",
|
|
||||||
"onLanguage:javascript",
|
"onLanguage:javascript",
|
||||||
"onLanguage:javascriptreact",
|
"onLanguage:javascriptreact",
|
||||||
"onLanguage:typescript",
|
"onLanguage:typescript",
|
||||||
"onLanguage:typescriptreact"
|
"onLanguage:typescriptreact",
|
||||||
|
"onLanguage:vue",
|
||||||
|
"onLanguage:svelte"
|
||||||
],
|
],
|
||||||
"main": "./out/main.js",
|
"main": "./out/main.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue