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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"jsPlugins": ["./plugin.js"],
"rules": {
"js-plugin/no-debugger": "error",
"no-unused-vars": "off"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
debugger;

let x;
21 changes: 21 additions & 0 deletions crates/oxc_language_server/fixtures/linter/js_plugins/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const plugin = {
meta: {
name: 'js-plugin',
},
rules: {
'no-debugger': {
create(context) {
return {
DebuggerStatement(debuggerStatement) {
context.report({
message: 'Unexpected Debugger Statement',
node: debuggerStatement,
});
},
};
},
},
},
};

export default plugin;
13 changes: 11 additions & 2 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ impl ServerLinter {

let base_patterns = oxlintrc.ignore_patterns.clone();

let mut external_plugin_store = ExternalPluginStore::default();
let mut external_plugin_store = ExternalPluginStore::new(false);
let config_builder =
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
.unwrap_or_default();
Expand Down Expand Up @@ -211,7 +211,7 @@ impl ServerLinter {
};
// Collect ignore patterns and their root
nested_ignore_patterns.push((oxlintrc.ignore_patterns.clone(), dir_path.to_path_buf()));
let mut external_plugin_store = ExternalPluginStore::default();
let mut external_plugin_store = ExternalPluginStore::new(false);
let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(
false,
oxlintrc,
Expand Down Expand Up @@ -669,4 +669,13 @@ mod test {
);
tester.test_and_snapshot_single_file("no-floating-promises/index.ts");
}

#[test]
fn test_ignore_js_plugins() {
let tester = Tester::new(
"fixtures/linter/js_plugins",
Some(LintOptions { run: Run::OnSave, ..Default::default() }),
);
tester.test_and_snapshot_single_file("index.js");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
source: crates/oxc_language_server/src/tester.rs
---
##########
file: fixtures/linter/js_plugins/index.js
----------

code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html"
message: "`debugger` statement is not allowed\nhelp: Remove the debugger statement"
range: Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 9 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/js_plugins/index.js"
related_information[0].location.range: Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: 9 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
fixed: Multiple(
[
FixedContent {
message: Some(
"Remove the debugger statement",
),
code: "",
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 9,
},
},
},
FixedContent {
message: Some(
"Disable no-debugger for this line",
),
code: "// oxlint-disable-next-line no-debugger\n",
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
},
FixedContent {
message: Some(
"Disable no-debugger for this file",
),
code: "// oxlint-disable no-debugger\n",
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
},
],
)
7 changes: 5 additions & 2 deletions crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ impl ConfigStoreBuilder {
}
}

if !external_plugins.is_empty() {
// If external plugins are not enabled (language server), then skip loading JS plugins.
// This is so that a project can use JS plugins via `oxlint` CLI, and language server
// will just silently ignore them - rather than crashing.
if !external_plugins.is_empty() && external_plugin_store.is_enabled() {
let Some(external_linter) = external_linter else {
#[expect(clippy::missing_panics_doc, reason = "infallible")]
let plugin_specifier = external_plugins.iter().next().unwrap().clone();
Expand Down Expand Up @@ -613,7 +616,7 @@ impl Display for ConfigBuilderError {
ConfigBuilderError::NoExternalLinterConfigured { plugin_specifier } => {
write!(
f,
"`jsPlugins` config contains '{plugin_specifier}'. JS plugins are not supported in the language server, or on 32-bit or big-endian platforms at present.",
"`jsPlugins` config contains '{plugin_specifier}'. JS plugins are not supported on 32-bit or big-endian platforms at present.",
)?;
Ok(())
}
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_linter/src/config/overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ pub struct OxlintOverride {
pub plugins: Option<LintPlugins>,

/// JS plugins for this override.
///
/// Note: JS plugins are experimental and not subject to semver.
/// They are not supported in language server at present.
#[serde(rename = "jsPlugins")]
pub external_plugins: Option<FxHashSet<String>>,

Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_linter/src/config/oxlintrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ use super::{
pub struct Oxlintrc {
pub plugins: Option<LintPlugins>,
/// JS plugins.
///
/// Note: JS plugins are experimental and not subject to semver.
/// They are not supported in language server at present.
#[serde(rename = "jsPlugins")]
pub external_plugins: Option<FxHashSet<String>>,
pub categories: OxlintCategories,
Expand Down
21 changes: 15 additions & 6 deletions crates/oxc_linter/src/config/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,21 @@ impl OxlintRules {
rules_to_replace.push((rule.read_json(config), severity));
}
} else {
let external_rule_id =
external_plugin_store.lookup_rule_id(plugin_name, rule_name)?;
external_rules_for_override
.entry(external_rule_id)
.and_modify(|sev| *sev = severity)
.or_insert(severity);
// If JS plugins are disabled (language server), assume plugin name refers to a JS plugin,
// and that rule name is valid for that plugin.
// But language server doesn't support JS plugins, so ignore the rule.
//
// This unfortunately means we can't catch genuinely invalid plugin names in language server
// (e.g. typos like `unicon/filename-case`). But we can't avoid this as the name of a JS plugin
// can only be known by loading it, which language server can't do at present.
if external_plugin_store.is_enabled() {
let external_rule_id =
external_plugin_store.lookup_rule_id(plugin_name, rule_name)?;
external_rules_for_override
.entry(external_rule_id)
.and_modify(|sev| *sev = severity)
.or_insert(severity);
}
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion crates/oxc_linter/src/external_plugin_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,40 @@ define_index_type! {
pub struct ExternalRuleId = u32;
}

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ExternalPluginStore {
registered_plugin_paths: FxHashSet<String>,

plugins: IndexVec<ExternalPluginId, ExternalPlugin>,
plugin_names: FxHashMap<String, ExternalPluginId>,
rules: IndexVec<ExternalRuleId, ExternalRule>,

// `true` for `oxlint`, `false` for language server
is_enabled: bool,
}

impl Default for ExternalPluginStore {
fn default() -> Self {
Self::new(true)
}
}

impl ExternalPluginStore {
pub fn new(is_enabled: bool) -> Self {
Self {
registered_plugin_paths: FxHashSet::default(),
plugins: IndexVec::default(),
plugin_names: FxHashMap::default(),
rules: IndexVec::default(),
is_enabled,
}
}

/// Returns `true` if external plugins are enabled.
pub fn is_enabled(&self) -> bool {
self.is_enabled
}

/// Returns `true` if no external plugins have been loaded.
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_linter/src/snapshots/schema_json.snap
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ expression: json
}
},
"jsPlugins": {
"description": "JS plugins.",
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"default": null,
"type": [
"array",
Expand Down Expand Up @@ -444,7 +444,7 @@ expression: json
]
},
"jsPlugins": {
"description": "JS plugins for this override.",
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
Expand Down
4 changes: 2 additions & 2 deletions npm/oxlint/configuration_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}
},
"jsPlugins": {
"description": "JS plugins.",
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"default": null,
"type": [
"array",
Expand Down Expand Up @@ -440,7 +440,7 @@
]
},
"jsPlugins": {
"description": "JS plugins for this override.",
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
Expand Down
6 changes: 6 additions & 0 deletions tasks/website/src/linter/snapshots/schema_markdown.snap
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ default: `null`

JS plugins.

Note: JS plugins are experimental and not subject to semver.
They are not supported in language server at present.


## overrides

Expand Down Expand Up @@ -233,6 +236,9 @@ type: `string[]`

JS plugins for this override.

Note: JS plugins are experimental and not subject to semver.
They are not supported in language server at present.


#### overrides[n].rules

Expand Down
Loading