diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index efb78dce0c90a..e2dcefa1b8687 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -11,7 +11,7 @@ pub mod cli { } pub use oxc_linter::{ - ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, PluginLoadResult, + ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, LintResult, PluginLoadResult, }; #[cfg(all(feature = "oxlint2", not(feature = "disable_oxlint2")))] diff --git a/crates/oxc_linter/src/config/config_store.rs b/crates/oxc_linter/src/config/config_store.rs index 5563671979cc5..b134d27cfc3bf 100644 --- a/crates/oxc_linter/src/config/config_store.rs +++ b/crates/oxc_linter/src/config/config_store.rs @@ -286,6 +286,10 @@ impl ConfigStore { } None } + + pub(crate) fn resolve_plugin_rule_names(&self, external_rule_id: u32) -> Option<(&str, &str)> { + self.external_plugin_store.resolve_plugin_rule_names(external_rule_id) + } } #[cfg(test)] diff --git a/crates/oxc_linter/src/context/host.rs b/crates/oxc_linter/src/context/host.rs index 2113c2997e709..fb633acb82a00 100644 --- a/crates/oxc_linter/src/context/host.rs +++ b/crates/oxc_linter/src/context/host.rs @@ -138,7 +138,7 @@ impl<'a> ContextHost<'a> { /// Add a diagnostic message to the end of the list of diagnostics. Can be used /// by any rule to report issues. #[inline] - pub(super) fn push_diagnostic(&self, diagnostic: Message<'a>) { + pub(crate) fn push_diagnostic(&self, diagnostic: Message<'a>) { self.diagnostics.borrow_mut().push(diagnostic); } diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index 62f7c917c6e8a..4431bb952b670 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -17,7 +17,9 @@ pub type ExternalLinterLoadPluginCb = Arc< >; pub type ExternalLinterCb = Arc< - dyn Fn(String, Vec) -> Result<(), Box> + Sync + Send, + dyn Fn(String, Vec) -> Result, Box> + + Sync + + Send, >; #[derive(Clone, Debug, Deserialize, Serialize)] @@ -26,6 +28,20 @@ pub enum PluginLoadResult { Failure(String), } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LintResult { + pub external_rule_id: u32, + pub message: String, + pub loc: Loc, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Loc { + pub start: u32, + pub end: u32, +} + #[derive(Clone)] #[cfg_attr(any(not(feature = "oxlint2"), feature = "disable_oxlint2"), expect(dead_code))] pub struct ExternalLinter { diff --git a/crates/oxc_linter/src/external_plugin_store.rs b/crates/oxc_linter/src/external_plugin_store.rs index aa3f852edadd5..42998bc588e90 100644 --- a/crates/oxc_linter/src/external_plugin_store.rs +++ b/crates/oxc_linter/src/external_plugin_store.rs @@ -107,6 +107,14 @@ impl ExternalPluginStore { } }) } + + pub fn resolve_plugin_rule_names(&self, external_rule_id: u32) -> Option<(&str, &str)> { + let external_rule = + self.rules.get(NonMaxU32::new(external_rule_id).map(ExternalRuleId)?)?; + let plugin = &self.plugins[external_rule.plugin_id]; + + Some((&plugin.name, &external_rule.name)) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -131,7 +139,6 @@ impl fmt::Display for ExternalRuleLookupError { impl std::error::Error for ExternalRuleLookupError {} #[derive(Debug, Default)] -#[expect(dead_code)] struct ExternalPlugin { name: String, rules: FxHashMap, diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 82d89ecf32e72..866798aa77b4b 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -26,7 +26,9 @@ pub mod table; use std::{path::Path, rc::Rc, sync::Arc}; +use oxc_diagnostics::OxcDiagnostic; use oxc_semantic::{AstNode, Semantic}; +use oxc_span::Span; pub use crate::{ config::{ @@ -35,7 +37,7 @@ pub use crate::{ }, context::LintContext, external_linter::{ - ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, PluginLoadResult, + ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, LintResult, PluginLoadResult, }, external_plugin_store::ExternalPluginStore, fixer::FixKind, @@ -52,7 +54,7 @@ pub use crate::{ use crate::{ config::{LintConfig, OxlintEnv, OxlintGlobals, OxlintSettings, ResolvedLinterState}, context::ContextHost, - fixer::{Fixer, Message}, + fixer::{Fixer, Message, PossibleFixes}, rules::RuleEnum, utils::iter_possible_jest_call_node, }; @@ -204,8 +206,31 @@ impl Linter { external_rules.iter().map(|(rule_id, _)| rule_id.as_u32()).collect(), ); match result { - Ok(()) => { - // TODO: report diagnostics + Ok(diagnostics) => { + for diagnostic in diagnostics { + match self.config.resolve_plugin_rule_names(diagnostic.external_rule_id) + { + Some((plugin_name, rule_name)) => { + ctx_host.push_diagnostic(Message::new( + // TODO: `error` isn't right, we need to get the severity from `external_rules` + OxcDiagnostic::error(diagnostic.message) + .with_label(Span::new( + diagnostic.loc.start, + diagnostic.loc.end, + )) + .with_error_code( + plugin_name.to_string(), + rule_name.to_string(), + ), + PossibleFixes::None, + )); + } + None => { + // TODO: report diagnostic, this should be unreachable + debug_assert!(false); + } + } + } } Err(_err) => { // TODO: report diagnostic diff --git a/napi/oxlint2/src/bindings.d.ts b/napi/oxlint2/src/bindings.d.ts index 59d667cd4f991..197f6c2453119 100644 --- a/napi/oxlint2/src/bindings.d.ts +++ b/napi/oxlint2/src/bindings.d.ts @@ -4,6 +4,6 @@ export type JsLoadPluginCb = ((arg: string) => Promise) export type JsRunCb = - ((arg0: string, arg1: Array) => void) + ((arg0: string, arg1: Array) => string) export declare function lint(loadPlugin: JsLoadPluginCb, run: JsRunCb): Promise diff --git a/napi/oxlint2/src/index.js b/napi/oxlint2/src/index.js index 881abbb8384d5..ddc590d852f44 100644 --- a/napi/oxlint2/src/index.js +++ b/napi/oxlint2/src/index.js @@ -28,7 +28,7 @@ class PluginRegistry { *getRules(ruleIds) { for (const ruleId of ruleIds) { - yield this.registeredRules[ruleId]; + yield { rule: this.registeredRules[ruleId], ruleId }; } } } @@ -71,6 +71,28 @@ class Linter { if (!Array.isArray(ruleIds) || ruleIds.length === 0) { throw new Error('Expected `ruleIds` to be a non-zero len array'); } + + const diagnostics = []; + + const createContext = (ruleId) => ({ + physicalFilename: filePath, + report: (diagnostic) => { + diagnostics.push({ + message: diagnostic.message, + loc: { start: diagnostic.node.start, end: diagnostic.node.end }, + externalRuleId: ruleId, + }); + }, + }); + + const rules = []; + for (const { rule, ruleId } of this.pluginRegistry.getRules(ruleIds)) { + rules.push(rule(createContext(ruleId))); + } + + // TODO: walk the AST + + return JSON.stringify(diagnostics); }; } diff --git a/napi/oxlint2/src/lib.rs b/napi/oxlint2/src/lib.rs index 6f2981d28bc21..2e46b86f15fe1 100644 --- a/napi/oxlint2/src/lib.rs +++ b/napi/oxlint2/src/lib.rs @@ -11,12 +11,18 @@ use napi::{ use napi_derive::napi; use oxlint::{ - ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, PluginLoadResult, + ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, LintResult, PluginLoadResult, lint as oxlint_lint, }; #[napi] -pub type JsRunCb = ThreadsafeFunction<(String, Vec), (), (String, Vec), Status, false>; +pub type JsRunCb = ThreadsafeFunction< + (String, Vec), + String, /* Vec */ + (String, Vec), + Status, + false, +>; #[napi] pub type JsLoadPluginCb = ThreadsafeFunction< @@ -39,11 +45,14 @@ fn wrap_run(cb: JsRunCb) -> ExternalLinterCb { ThreadsafeFunctionCallMode::NonBlocking, move |result, _env| { let _ = match &result { - Ok(()) => tx.send(Ok(())), + Ok(r) => match serde_json::from_str::>(r) { + Ok(v) => tx.send(Ok(v)), + Err(_e) => tx.send(Err("Failed to deserialize lint result".to_string())), + }, Err(e) => tx.send(Err(e.to_string())), }; - result + result.map(|_| ()) }, ); @@ -52,7 +61,7 @@ fn wrap_run(cb: JsRunCb) -> ExternalLinterCb { } match rx.recv() { - Ok(Ok(())) => Ok(()), + Ok(Ok(x)) => Ok(x), Ok(Err(e)) => Err(format!("Callback reported error: {e}").into()), Err(e) => Err(format!("Callback did not respond: {e}").into()), } diff --git a/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap b/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap index 1fb8bbdc27c0a..193ea0977f5de 100644 --- a/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap +++ b/napi/oxlint2/test/__snapshots__/e2e.test.ts.snap @@ -19,7 +19,21 @@ Finished in Xms on 1 file using X threads." `; exports[`cli options for bundling > should load a custom plugin 1`] = ` -"Found 0 warnings and 0 errors. +" + ! ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html\\eslint(no-debugger)]8;;\\: \`debugger\` statement is not allowed + ,-[index.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + \`---- + help: Remove the debugger statement + + x basic-custom-plugin(no-debugger): Unexpected Debugger Statement + ,-[index.js:1:1] + 1 | debugger; + : ^ + \`---- + +Found 1 warning and 1 error. Finished in Xms on 1 file using X threads." `; diff --git a/napi/oxlint2/test/e2e.test.ts b/napi/oxlint2/test/e2e.test.ts index 2e99206e55bf1..5840af27d1924 100644 --- a/napi/oxlint2/test/e2e.test.ts +++ b/napi/oxlint2/test/e2e.test.ts @@ -44,7 +44,7 @@ describe('cli options for bundling', () => { 'test/fixtures/basic_custom_plugin', ); - expect(exitCode).toBe(0); + expect(exitCode).toBe(1); expect(normalizeOutput(stdout)).toMatchSnapshot(); }); diff --git a/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json b/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json index 97cb03b2bcd7d..9c8cb3cddcbd8 100644 --- a/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json +++ b/napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json @@ -1,8 +1,7 @@ { - "plugins": [ - "./test_plugin" - ], + "plugins": ["./test_plugin"], "rules": { "basic-custom-plugin/no-debugger": "error" - } + }, + "ignorePatterns": ["test_plugin"] } diff --git a/napi/oxlint2/test/fixtures/basic_custom_plugin/index.js b/napi/oxlint2/test/fixtures/basic_custom_plugin/index.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/napi/oxlint2/test/fixtures/basic_custom_plugin/index.js @@ -0,0 +1 @@ +debugger; diff --git a/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js b/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js index 2d0676aded1a2..170d08e5c0595 100644 --- a/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js +++ b/napi/oxlint2/test/fixtures/basic_custom_plugin/test_plugin/index.js @@ -3,11 +3,14 @@ export default { name: "basic-custom-plugin", }, rules: { - "no-debugger": (_context) => { + "no-debugger": (context) => { + // TODO: move this call into `DebuggerStatement`, once we are walking the ast. + context.report({ + message: "Unexpected Debugger Statement", + node: { start: 0, end: 0 }, + }); return { - DebuggerStatement(_debuggerStatement) { - throw new Error("unimplemented"); - }, + DebuggerStatement(_debuggerStatement) {}, }; }, },