Skip to content

Commit 1f54cf4

Browse files
committed
wip: skip rules that do not have any relevant node types
1 parent f6b3a21 commit 1f54cf4

File tree

13 files changed

+4724
-25
lines changed

13 files changed

+4724
-25
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ benchmark = "bench -p oxc_benchmark"
88
minsize = "run -p oxc_minsize --profile coverage --"
99
allocs = "run -p oxc_track_memory_allocations --profile coverage --"
1010
rule = "run -p rulegen"
11+
lintgen = "run -p oxc_linter_codegen"
1112

1213
# Build oxlint in release mode
1314
oxlint = "build --release -p oxlint --bin oxlint --features allocator"

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,30 @@ jobs:
353353
cargo run -p oxc_ast_tools
354354
git diff --exit-code ||
355355
(echo 'AST changes caused the "generated" code to get outdated. Have you forgotten to run the `just ast` command and/or commit generated codes?' && exit 1)
356+
357+
lintgen:
358+
name: Linter changes
359+
runs-on: ubuntu-latest
360+
steps:
361+
- uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1
362+
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
363+
id: filter
364+
with:
365+
filters: |
366+
src:
367+
- '.github/workflows/ci.yml'
368+
- 'crates/oxc_linter/**'
369+
- 'tasks/linter_codegen/**'
370+
371+
- uses: oxc-project/setup-rust@cd82e1efec7fef815e2c23d296756f31c7cdc03d # v1.0.0
372+
if: steps.filter.outputs.src == 'true'
373+
with:
374+
cache-key: lintgen
375+
save-cache: ${{ github.ref_name == 'main' }}
376+
377+
- name: Check linter changes
378+
if: steps.filter.outputs.src == 'true'
379+
run: |
380+
cargo lintgen
381+
git diff --exit-code ||
382+
(echo 'Linter codegen has changed. Run the `cargo lintgen` command to update the linter code generated and commit it.' && exit 1)

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_linter/src/lib.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::{path::Path, rc::Rc, sync::Arc};
55

66
use oxc_allocator::Allocator;
7+
use oxc_ast::ast_kind::AST_TYPE_MAX;
78
use oxc_semantic::{AstNode, Semantic};
89

910
#[cfg(all(feature = "oxlint2", not(feature = "disable_oxlint2")))]
@@ -25,6 +26,7 @@ mod module_graph_visitor;
2526
mod module_record;
2627
mod options;
2728
mod rule;
29+
mod rule_runner_impls;
2830
mod service;
2931
mod tsgolint;
3032
mod utils;
@@ -56,7 +58,7 @@ pub use crate::{
5658
module_record::ModuleRecord,
5759
options::LintOptions,
5860
options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind},
59-
rule::{RuleCategory, RuleFixMeta, RuleMeta},
61+
rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleRunner},
6062
service::{LintService, LintServiceOptions, RuntimeFileSystem},
6163
tsgolint::TsGoLintState,
6264
utils::{read_to_arena_str, read_to_string},
@@ -167,7 +169,28 @@ impl Linter {
167169
// Collect rules into a Vec so that we can iterate over the rules multiple times
168170
let rules = rules.collect::<Vec<_>>();
169171

172+
// TODO: It seems like there is probably a more intelligent way to preallocate space here. This will
173+
// likely incur quite a few unnecessary reallocs currently. We theoretically could compute this at
174+
// compile-time since we know all of the rules and their AST node type information ahead of time.
175+
let mut rules_by_ast_type = vec![Vec::new(); AST_TYPE_MAX as usize + 1];
176+
// TODO: Compute needed capacity. This is a slight overestimate as not 100% of rules will need to run on all
177+
// node types, but it at least guarantees we won't need to realloc.
178+
let mut rules_any_ast_type = Vec::with_capacity(rules.len());
179+
170180
for (rule, ctx) in &rules {
181+
// Collect node type information for rules. In large files, benchmarking showed it was worth
182+
// collecting rules into buckets by AST node type to avoid iterating over all rules for each node.
183+
if rule.should_run(&ctx_host) {
184+
let (ast_types, all_types) = rule.types_info();
185+
if all_types {
186+
rules_any_ast_type.push((rule, ctx));
187+
} else {
188+
for ty in ast_types {
189+
rules_by_ast_type[ty as usize].push((rule, ctx));
190+
}
191+
}
192+
}
193+
171194
rule.run_once(ctx);
172195
}
173196

@@ -177,8 +200,12 @@ impl Linter {
177200
}
178201
}
179202

203+
// Run rules on nodes
180204
for node in semantic.nodes() {
181-
for (rule, ctx) in &rules {
205+
for (rule, ctx) in &rules_by_ast_type[node.kind().ty() as usize] {
206+
rule.run(node, ctx);
207+
}
208+
for (rule, ctx) in &rules_any_ast_type {
182209
rule.run(node, ctx);
183210
}
184211
}
@@ -198,8 +225,19 @@ impl Linter {
198225
rule.run_on_symbol(symbol, ctx);
199226
}
200227

201-
for node in semantic.nodes() {
202-
rule.run(node, ctx);
228+
// For smaller files, benchmarking showed it was faster to iterate over all rules and just check the
229+
// node types as we go, rather than pre-bucketing rules by AST node type and doing extra allocations.
230+
let (ast_types, all_types) = rule.types_info();
231+
if all_types {
232+
for node in semantic.nodes() {
233+
rule.run(node, ctx);
234+
}
235+
} else {
236+
for node in semantic.nodes() {
237+
if ast_types.has(node.kind().ty()) {
238+
rule.run(node, ctx);
239+
}
240+
}
203241
}
204242

205243
if should_run_on_jest_node {

crates/oxc_linter/src/rule.rs

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{fmt, hash::Hash};
55
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
66
use serde::{Deserialize, Serialize};
77

8-
use oxc_semantic::SymbolId;
8+
use oxc_semantic::{AstTypesBitset, SymbolId};
99

1010
use crate::{
1111
AstNode, FixKind,
@@ -66,6 +66,16 @@ pub trait Rule: Sized + Default + fmt::Debug {
6666
}
6767
}
6868

69+
pub trait RuleRunner: Rule {
70+
/// `AstType`s that rule acts on
71+
const NODE_TYPES: &AstTypesBitset;
72+
/// `true` if codegen can't figure out what node types rule acts on
73+
const ANY_NODE_TYPE: bool;
74+
75+
fn types_info(&self) -> (&'static AstTypesBitset, bool) {
76+
(Self::NODE_TYPES, Self::ANY_NODE_TYPE)
77+
}
78+
}
6979
pub trait RuleMeta {
7080
const NAME: &'static str;
7181

@@ -279,6 +289,8 @@ impl From<RuleFixMeta> for FixKind {
279289

280290
#[cfg(test)]
281291
mod test {
292+
use crate::{RuleMeta, RuleRunner};
293+
282294
use super::RuleCategory;
283295

284296
#[test]
@@ -322,4 +334,83 @@ mod test {
322334
assert_eq!(de, RuleCategory::try_from(input).unwrap(), "{input}");
323335
}
324336
}
337+
338+
#[test]
339+
fn test_rule_runner_impls() {
340+
use crate::rules::*;
341+
use oxc_ast::AstType::*;
342+
343+
// The RuleRunner code is automatically generated by the `oxc_linter_codegen` crate.
344+
// This is set of manually verified test cases to ensure that the generated code
345+
// is working as expected and is not skipping rules for nodes that actually should be linted.
346+
assert_rule_runs_on_node_types(&eslint::no_debugger::NoDebugger, &[DebuggerStatement]);
347+
assert_rule_runs_on_node_types(&eslint::no_with::NoWith, &[WithStatement]);
348+
assert_rule_runs_on_node_types(
349+
&eslint::curly::Curly::default(),
350+
&[
351+
IfStatement,
352+
ForStatement,
353+
ForInStatement,
354+
ForOfStatement,
355+
WhileStatement,
356+
DoWhileStatement,
357+
],
358+
);
359+
assert_rule_runs_on_node_types(
360+
&eslint::default_case::DefaultCase::default(),
361+
&[SwitchStatement],
362+
);
363+
assert_rule_runs_on_node_types(
364+
&import::no_mutable_exports::NoMutableExports,
365+
&[ExportNamedDeclaration, ExportDefaultDeclaration],
366+
);
367+
assert_rule_runs_on_node_types(
368+
&jest::prefer_jest_mocked::PreferJestMocked,
369+
&[TSAsExpression, TSTypeAssertion],
370+
);
371+
assert_rule_runs_on_node_types(
372+
&jsx_a11y::anchor_is_valid::AnchorIsValid::default(),
373+
&[JSXElement],
374+
);
375+
assert_rule_runs_on_node_types(
376+
&nextjs::no_head_element::NoHeadElement,
377+
&[JSXOpeningElement],
378+
);
379+
assert_rule_runs_on_node_types(
380+
&oxc::bad_bitwise_operator::BadBitwiseOperator,
381+
&[BinaryExpression, AssignmentExpression],
382+
);
383+
assert_rule_runs_on_node_types(
384+
&promise::prefer_await_to_callbacks::PreferAwaitToCallbacks,
385+
&[CallExpression, Function, ArrowFunctionExpression],
386+
);
387+
assert_rule_runs_on_node_types(
388+
&react::button_has_type::ButtonHasType::default(),
389+
&[JSXOpeningElement, CallExpression],
390+
);
391+
assert_rule_runs_on_node_types(
392+
&typescript::ban_types::BanTypes,
393+
&[TSTypeReference, TSTypeLiteral],
394+
);
395+
assert_rule_runs_on_node_types(
396+
&unicorn::explicit_length_check::ExplicitLengthCheck::default(),
397+
&[StaticMemberExpression],
398+
);
399+
}
400+
401+
fn assert_rule_runs_on_node_types<R: RuleMeta + RuleRunner>(
402+
rule: &R,
403+
node_types: &[oxc_ast::AstType],
404+
) {
405+
let (types, _) = rule.types_info();
406+
assert!(!R::ANY_NODE_TYPE, "{} should not have ANY_NODE_TYPE set to true", R::NAME);
407+
for node_type in node_types {
408+
assert!(
409+
types.has(*node_type),
410+
"{}: missing {:?} in its NODE_TYPES (this means it will incorrectly skip nodes it needs to lint)",
411+
R::NAME,
412+
node_type
413+
);
414+
}
415+
}
325416
}

0 commit comments

Comments
 (0)