refactor(linter): compute lintable extensions at compile time (#10090)

Previously, we were wasting some time creating a `Vec` at runtime with the lintable extensions, which is all of the JS/TS extensions plus other frameworks extensions like `.vue`. Now, we precompute this as a static string slice at compile time. This also ensures that we are using the same extensions list between `oxlint` and the language server too. Theoretically this is a very minor perf win possibly?
This commit is contained in:
camchenry committed 2025-03-29 12:38:23 +00:00
1 parent 1ab88718a8
commit d8e49a15d9
22 files changed
+199 -37

No files matched your search

+1 -1
View File
@@ -5,6 +5,6 @@
"tamasfe.even-better-toml", // toml syntax
"nefrob.vscode-just-syntax", // justfile syntax & task runner
"tekumara.typos-vscode", // spell checker
"EditorConfig.EditorConfig", // editorconfig
"EditorConfig.EditorConfig" // editorconfig
]
}
Generated
+7 -1
View File
@@ -319,6 +319,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "constcat"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffb5df6dd5dadb422897e8132f415d7a054e3cd757e5070b663f75bea1840fb"
[[package]]
name = "convert_case"
version = "0.8.0"
@@ -1809,7 +1815,6 @@ dependencies = [
"oxc_linter",
"oxc_parser",
"oxc_semantic",
"oxc_span",
"papaya",
"rustc-hash",
"serde",
@@ -1823,6 +1828,7 @@ name = "oxc_linter"
version = "0.16.3"
dependencies = [
"bitflags 2.9.0",
"constcat",
"convert_case",
"cow-utils",
"fast-glob",
+1
View File
@@ -168,6 +168,7 @@ bpaf = "0.9.16"
bumpalo = "=3.17.0"
compact_str = "0.9.0"
console = "0.15.10"
constcat = "0.6.0"
convert_case = "0.8.0"
cow-utils = "0.1.3"
criterion2 = { version = "3.0.0", default-features = false }
+3 -10
View File
@@ -10,16 +10,15 @@ use ignore::{gitignore::Gitignore, overrides::OverrideBuilder};
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic};
use oxc_linter::{
AllowWarnDeny, ConfigStore, ConfigStoreBuilder, InvalidFilterKind, LintFilter, LintOptions,
LintService, LintServiceOptions, Linter, Oxlintrc, loader::LINT_PARTIAL_LOADER_EXT,
LintService, LintServiceOptions, Linter, Oxlintrc,
};
use oxc_span::VALID_EXTENSIONS;
use rustc_hash::{FxHashMap, FxHashSet};
use serde_json::Value;
use crate::{
cli::{CliRunResult, LintCommand, MiscOptions, ReportUnusedDirectives, Runner, WarningOptions},
output_formatter::{LintCommandInfo, OutputFormatter},
walk::{Extensions, Walk},
walk::Walk,
};
#[derive(Debug)]
@@ -80,12 +79,6 @@ impl Runner for LintRunner {
}
};
let extensions = VALID_EXTENSIONS
.iter()
.chain(LINT_PARTIAL_LOADER_EXT.iter())
.copied()
.collect::<Vec<&'static str>>();
let config_search_result =
Self::find_oxlint_config(&self.cwd, basic_options.config.as_ref());
@@ -172,7 +165,7 @@ impl Runner for LintRunner {
}
let walker = Walk::new(&paths, &ignore_options, override_builder);
let paths = walker.with_extensions(Extensions(extensions)).paths();
let paths = walker.paths();
let number_of_files = paths.len();
+3 -2
View File
@@ -1,7 +1,7 @@
use std::{ffi::OsStr, path::PathBuf, sync::Arc, sync::mpsc};
use ignore::{DirEntry, overrides::Override};
use oxc_span::VALID_EXTENSIONS;
use oxc_linter::LINTABLE_EXTENSIONS;
use crate::cli::IgnoreOptions;
@@ -10,7 +10,7 @@ pub struct Extensions(pub Vec<&'static str>);
impl Default for Extensions {
fn default() -> Self {
Self(VALID_EXTENSIONS.to_vec())
Self(LINTABLE_EXTENSIONS.to_vec())
}
}
@@ -108,6 +108,7 @@ impl Walk {
receiver.into_iter().flatten().collect()
}
#[cfg_attr(not(test), expect(dead_code))]
pub fn with_extensions(mut self, extensions: Extensions) -> Self {
self.extensions = extensions;
self
-1
View File
@@ -28,7 +28,6 @@ oxc_diagnostics = { workspace = true }
oxc_linter = { workspace = true }
oxc_parser = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true }
cow-utils = { workspace = true }
env_logger = { workspace = true, features = ["humantime"] }
@@ -0,0 +1,20 @@
---
debugger
---
<!-- Store the message prop as a data attribute. -->
<astro-greet data-message={message}>
<button>Say hi!</button>
</astro-greet>
<script asdf >
debugger
</script>
<script asdf>
debugger
</script>
<script>
debugger
</script>
@@ -0,0 +1,23 @@
<script>
debugger;
export let title;
export let person;
// this will update `document.title` whenever
// the `title` prop changes
$: document.title = title;
$: {
console.log(`multiple statements can be combined`);
console.log(`the current title is ${title}`);
}
// this will update `name` when 'person' changes
$: ({ name } = person);
// don't do this. it will run before the previous line
let name2 = name;
</script>
<h1>Hello {name}!</h1>
@@ -0,0 +1,12 @@
<template>
<div>Hello World</div>
</template>
<script>
debugger
</script>
<script setup lang="ts" generic="T extends Record<string, string>">
let foo: T; // test ts syntax
debugger;
</script>
@@ -12,12 +12,11 @@ use tower_lsp::lsp_types::{self, DiagnosticRelatedInformation, DiagnosticSeverit
use oxc_allocator::Allocator;
use oxc_diagnostics::{Error, NamedSource};
use oxc_linter::{
Linter, ModuleRecord,
loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXT, Loader},
LINTABLE_EXTENSIONS, Linter, ModuleRecord,
loader::{JavaScriptSource, Loader},
};
use oxc_parser::{ParseOptions, Parser};
use oxc_semantic::SemanticBuilder;
use oxc_span::VALID_EXTENSIONS;
use crate::DiagnosticReport;
use crate::linter::error_with_position::{ErrorReport, ErrorWithPosition, FixedContent};
@@ -187,9 +186,8 @@ impl IsolatedLintHandler {
fn should_lint_path(path: &Path) -> bool {
static WANTED_EXTENSIONS: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
let wanted_exts = WANTED_EXTENSIONS.get_or_init(|| {
VALID_EXTENSIONS.iter().chain(LINT_PARTIAL_LOADER_EXT.iter()).copied().collect()
});
let wanted_exts =
WANTED_EXTENSIONS.get_or_init(|| LINTABLE_EXTENSIONS.iter().copied().collect());
path.extension()
.and_then(std::ffi::OsStr::to_str)
@@ -90,4 +90,11 @@ mod test {
Tester::new_with_linter(linter)
.test_and_snapshot_single_file("fixtures/linter/regexp_feature/index.ts");
}
#[test]
fn test_frameworks() {
Tester::new().test_and_snapshot_single_file("fixtures/linter/astro/debugger.astro");
Tester::new().test_and_snapshot_single_file("fixtures/linter/vue/debugger.vue");
Tester::new().test_and_snapshot_single_file("fixtures/linter/svelte/debugger.svelte");
}
}
@@ -0,0 +1,49 @@
---
source: crates/oxc_language_server/src/linter/tester.rs
---
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 8 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/astro/debugger.astro"
related_information[0].location.range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 8 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 10, character: 2 }, end: Position { line: 10, character: 10 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/astro/debugger.astro"
related_information[0].location.range: Range { start: Position { line: 10, character: 2 }, end: Position { line: 10, character: 10 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 14, character: 2 }, end: Position { line: 14, character: 10 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/astro/debugger.astro"
related_information[0].location.range: Range { start: Position { line: 14, character: 2 }, end: Position { line: 14, character: 10 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 18, character: 2 }, end: Position { line: 18, character: 10 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/astro/debugger.astro"
related_information[0].location.range: Range { start: Position { line: 18, character: 2 }, end: Position { line: 18, character: 10 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
@@ -0,0 +1,13 @@
---
source: crates/oxc_language_server/src/linter/tester.rs
---
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 1, character: 1 }, end: Position { line: 1, character: 10 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/svelte/debugger.svelte"
related_information[0].location.range: Range { start: Position { line: 1, character: 1 }, end: Position { line: 1, character: 10 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
@@ -0,0 +1,25 @@
---
source: crates/oxc_language_server/src/linter/tester.rs
---
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 5, character: 4 }, end: Position { line: 5, character: 12 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/vue/debugger.vue"
related_information[0].location.range: Range { start: Position { line: 5, character: 4 }, end: Position { line: 5, character: 12 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger"
message: "`debugger` statement is not allowed\nhelp: Delete this code."
range: Range { start: Position { line: 10, character: 4 }, end: Position { line: 10, character: 13 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/vue/debugger.vue"
related_information[0].location.range: Range { start: Position { line: 10, character: 4 }, end: Position { line: 10, character: 13 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
@@ -102,8 +102,10 @@ impl Tester<'_> {
#[expect(clippy::disallowed_methods)]
pub fn test_and_snapshot_single_file(&self, relative_file_path: &str) {
let uri = get_file_uri(relative_file_path);
let content = std::fs::read_to_string(uri.to_file_path().unwrap())
.expect("could not read fixture file");
let content = match std::fs::read_to_string(uri.to_file_path().unwrap()) {
Ok(content) => content,
Err(err) => panic!("could not read fixture file: {err}: {relative_file_path}"),
};
let reports = self.server_linter.run_single(&uri, Some(content)).unwrap();
let snapshot = if reports.is_empty() {
"No diagnostic reports".to_string()
+1
View File
@@ -41,6 +41,7 @@ oxc_syntax = { workspace = true, features = ["serialize"] }
#
bitflags = { workspace = true }
constcat = { workspace = true }
convert_case = { workspace = true }
cow-utils = { workspace = true }
fast-glob = { workspace = true }
+1
View File
@@ -38,6 +38,7 @@ pub use crate::{
context::LintContext,
fixer::FixKind,
frameworks::FrameworkFlags,
loader::LINTABLE_EXTENSIONS,
module_record::ModuleRecord,
options::LintOptions,
options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind},
+2 -2
View File
@@ -4,7 +4,7 @@ use oxc_span::SourceType;
mod partial_loader;
mod source;
pub use partial_loader::{LINT_PARTIAL_LOADER_EXT, PartialLoader};
pub use partial_loader::{LINT_PARTIAL_LOADER_EXTENSIONS, LINTABLE_EXTENSIONS, PartialLoader};
pub use source::JavaScriptSource;
// TODO: use oxc_resolver::FileSystem. We can't do so until that crate exposes FileSystemOs
@@ -19,7 +19,7 @@ impl Loader {
|| path
.extension()
.and_then(std::ffi::OsStr::to_str)
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXT.contains(&ext))
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXTENSIONS.contains(&ext))
}
/// # Errors
@@ -2,13 +2,22 @@ mod astro;
mod svelte;
mod vue;
use oxc_span::VALID_EXTENSIONS;
pub use self::{astro::AstroPartialLoader, svelte::SveltePartialLoader, vue::VuePartialLoader};
use crate::loader::JavaScriptSource;
const SCRIPT_START: &str = "<script";
const SCRIPT_END: &str = "</script>";
pub const LINT_PARTIAL_LOADER_EXT: &[&str] = &["vue", "astro", "svelte"];
/// File extensions that can contain JS/TS code in certain parts, such as in `<script>` tags, and can
/// be loaded using the [`PartialLoader`].
pub const LINT_PARTIAL_LOADER_EXTENSIONS: &[&str] = &["vue", "astro", "svelte"];
/// All valid JavaScript/TypeScript extensions, plus additional framework files that
/// contain JavaScript/TypeScript code in them (e.g., Vue, Astro, Svelte, etc.).
pub const LINTABLE_EXTENSIONS: &[&str] =
constcat::concat_slices!([&str]: VALID_EXTENSIONS, LINT_PARTIAL_LOADER_EXTENSIONS);
pub struct PartialLoader;
@@ -4,7 +4,10 @@ use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{
context::LintContext, loader::LINT_PARTIAL_LOADER_EXT, rule::Rule, utils::is_empty_stmt,
context::{ContextHost, LintContext},
loader::LINT_PARTIAL_LOADER_EXTENSIONS,
rule::Rule,
utils::is_empty_stmt,
};
fn no_empty_file_diagnostic(span: Span) -> OxcDiagnostic {
@@ -39,13 +42,6 @@ declare_oxc_lint!(
impl Rule for NoEmptyFile {
fn run_once(&self, ctx: &LintContext) {
if ctx
.file_path()
.extension()
.is_some_and(|ext| LINT_PARTIAL_LOADER_EXT.contains(&ext.to_string_lossy().as_ref()))
{
return;
}
let Some(root) = ctx.nodes().root_node() else {
return;
};
@@ -67,6 +63,12 @@ impl Rule for NoEmptyFile {
span.end = std::cmp::min(span.end, 100);
ctx.diagnostic(no_empty_file_diagnostic(span));
}
fn should_run(&self, ctx: &ContextHost) -> bool {
ctx.file_path().extension().is_some_and(|ext| {
!LINT_PARTIAL_LOADER_EXTENSIONS.contains(&ext.to_string_lossy().as_ref())
})
}
}
fn has_triple_slash_directive(ctx: &LintContext<'_>) -> bool {
+2 -2
View File
@@ -25,7 +25,7 @@ use oxc_span::{CompactStr, SourceType, VALID_EXTENSIONS};
use super::LintServiceOptions;
use crate::{
Fixer, Linter, Message,
loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXT, PartialLoader},
loader::{JavaScriptSource, LINT_PARTIAL_LOADER_EXTENSIONS, PartialLoader},
module_record::ModuleRecord,
utils::read_to_string,
};
@@ -175,7 +175,7 @@ impl Runtime {
) -> Option<Result<(SourceType, String), Error>> {
let source_type = SourceType::from_path(path);
let not_supported_yet =
source_type.as_ref().is_err_and(|_| !LINT_PARTIAL_LOADER_EXT.contains(&ext));
source_type.as_ref().is_err_and(|_| !LINT_PARTIAL_LOADER_EXTENSIONS.contains(&ext));
if not_supported_yet {
return None;
}
+1 -1
View File
@@ -97,7 +97,7 @@ impl ContentEq for SourceType {
}
/// Valid file extensions
pub const VALID_EXTENSIONS: [&str; 8] = ["js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx"];
pub const VALID_EXTENSIONS: &[&str] = &["js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx"];
impl SourceType {
/// Creates a [`SourceType`] representing a regular [`JavaScript`] file.