Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/oxlint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")))]
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/config/config_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/context/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
18 changes: 17 additions & 1 deletion crates/oxc_linter/src/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ pub type ExternalLinterLoadPluginCb = Arc<
>;

pub type ExternalLinterCb = Arc<
dyn Fn(String, Vec<u32>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> + Sync + Send,
dyn Fn(String, Vec<u32>) -> Result<Vec<LintResult>, Box<dyn std::error::Error + Send + Sync>>
+ Sync
+ Send,
>;

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand All @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion crates/oxc_linter/src/external_plugin_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<String, ExternalRuleId>,
Expand Down
33 changes: 29 additions & 4 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,
Expand All @@ -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,
};
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion napi/oxlint2/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export type JsLoadPluginCb =
((arg: string) => Promise<string>)

export type JsRunCb =
((arg0: string, arg1: Array<number>) => void)
((arg0: string, arg1: Array<number>) => string)

export declare function lint(loadPlugin: JsLoadPluginCb, run: JsRunCb): Promise<boolean>
24 changes: 23 additions & 1 deletion napi/oxlint2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PluginRegistry {

*getRules(ruleIds) {
for (const ruleId of ruleIds) {
yield this.registeredRules[ruleId];
yield { rule: this.registeredRules[ruleId], ruleId };
}
}
}
Expand Down Expand Up @@ -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);
};
}

Expand Down
19 changes: 14 additions & 5 deletions napi/oxlint2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>), (), (String, Vec<u32>), Status, false>;
pub type JsRunCb = ThreadsafeFunction<
(String, Vec<u32>),
String, /* Vec<LintResult> */
(String, Vec<u32>),
Status,
false,
>;

#[napi]
pub type JsLoadPluginCb = ThreadsafeFunction<
Expand All @@ -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::<Vec<LintResult>>(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(|_| ())
},
);

Expand All @@ -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()),
}
Expand Down
16 changes: 15 additions & 1 deletion napi/oxlint2/test/__snapshots__/e2e.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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."
`;

Expand Down
2 changes: 1 addition & 1 deletion napi/oxlint2/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
7 changes: 3 additions & 4 deletions napi/oxlint2/test/fixtures/basic_custom_plugin/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"plugins": [
"./test_plugin"
],
"plugins": ["./test_plugin"],
"rules": {
"basic-custom-plugin/no-debugger": "error"
}
},
"ignorePatterns": ["test_plugin"]
}
1 change: 1 addition & 0 deletions napi/oxlint2/test/fixtures/basic_custom_plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debugger;
Original file line number Diff line number Diff line change
Expand Up @@ -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) {},
};
},
},
Expand Down
Loading