mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
> Re-creation of #4427 due to rebasing issues. Original attempt: #642 ----- Third time's the charm? Each time I attempt this rule, I find a bunch of bugs in `Semantic`, and I expect this attempt to be no different. Expect sidecar issues+PRs stemming from this PR here. ## Not Supported These are cases supported in the original eslint rule, but that I'm intentionally deciding not to support - export comments in scripts ```js /* exported a */ var a; ``` - global comments ```js /* global a */ var a; ``` ## Behavior Changes These are intentional deviations from the original rule's behavior: - logical re-assignments are not considered usages ```js // passes in eslint/no-unused-vars, fails in this implementation let a = 0; a ||= 1; let b = 0; b &&= 2; let c = undefined; c ??= [] ``` ## Known Limitations - Lint rules do not have babel or tsconfig information, meaning we can't determine if `React` imports are being used or not. The relevant tsconfig settings here are `jsx`, `jsxPragma`, and `jsxFragmentName`. To accommodate this, all imports to symbols named `React` or `h` are ignored in JSX files. - References to symbols used in JSDoc `{@link}` tags are not created, so symbols that are only used in doc comments will be reported as unused. See: #4443 - `.vue` files are skipped completely, since variables can be used in templates in ways we cannot detect > note: `.d.ts` files are skipped as well. ## Todo - [x] Skip unused TS enum members on used enums - [x] Skip unused parameters followed by used variables in object/array spreads - [x] Re-assignments to array/object spreads do not respect `destructuredArrayIgnorePattern` (related to: https://github.com/oxc-project/oxc/issues/4435) - [x] #4493 - [x] References inside a nested scope are not considered usages (#4447) - [x] Port over typescript-eslint test cases _(wip, they've been copied and I'm slowly enabling them)_ - [x] Handle constructor properties ```ts class Foo { constructor(public a) {} // `a` should be allowed } ``` - [x] Read references in sequence expressions (that are not in the last position) should not count as a usage ```js let a = 0; let b = (a++, 0); console.log(b) ``` > Honestly, is anyone even writing code like this? - [x] function overload signatures should not be reported - [x] Named functions returned from other functions get incorrectly reported as unused (found by @camc314) ```js function foo() { return function bar() { } } Foo()() ``` - [x] false positive for TS modules within ambient modules ```ts declare global { // incorrectly marked as unused namespace jest { } } ``` ## Blockers - https://github.com/oxc-project/oxc/issues/4436 - https://github.com/oxc-project/oxc/issues/4437 - #4446 - #4447 - #4494 - #4495 ## Non-Blocking Issues - #4443 - #4475 (prevents checks on exported symbols from namespaces)
199 lines
4.9 KiB
Rust
199 lines
4.9 KiB
Rust
use oxc_ast::AstKind;
|
|
use oxc_semantic::{ScopeFlags, SymbolFlags};
|
|
|
|
use crate::util::{Expect, SemanticTester};
|
|
|
|
#[test]
|
|
fn test_top_level_strict() {
|
|
// Module with top-level "use strict"
|
|
SemanticTester::js(
|
|
r#"
|
|
"use strict";
|
|
function foo() {
|
|
return 1
|
|
}
|
|
"#,
|
|
)
|
|
.has_root_symbol("foo")
|
|
.is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
|
|
// .expect(expect_strict)
|
|
.test();
|
|
|
|
// Module without top-level "use strict"
|
|
SemanticTester::js(
|
|
r"
|
|
function foo() {
|
|
return 1
|
|
}
|
|
",
|
|
)
|
|
.has_root_symbol("foo")
|
|
.is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
|
|
.test();
|
|
|
|
// Script with top-level "use strict"
|
|
SemanticTester::js(
|
|
r#"
|
|
"use strict";
|
|
function foo() {
|
|
return 1
|
|
}
|
|
"#,
|
|
)
|
|
.with_module(false)
|
|
.has_root_symbol("foo")
|
|
.is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
|
|
.test();
|
|
|
|
// Script without top-level "use strict"
|
|
SemanticTester::js(
|
|
r"
|
|
function foo() {
|
|
return 1
|
|
}
|
|
",
|
|
)
|
|
.with_module(false)
|
|
.has_root_symbol("foo")
|
|
.is_in_scope(ScopeFlags::Top)
|
|
.is_not_in_scope(ScopeFlags::StrictMode)
|
|
.test();
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_level_strict() {
|
|
let tester = SemanticTester::js(
|
|
r#"
|
|
function foo() {
|
|
"use strict";
|
|
let x = 1;
|
|
return x
|
|
}
|
|
"#,
|
|
)
|
|
.with_module(false);
|
|
|
|
tester.has_some_symbol("x")
|
|
.is_in_scope(ScopeFlags::StrictMode | ScopeFlags::Function)
|
|
.expect(|(semantic, symbol_id)| -> Result<(), &'static str> {
|
|
let scope_id = semantic.symbol_scope(symbol_id);
|
|
let Some(parent_scope_id) = semantic.scopes().get_parent_id(scope_id) else {
|
|
return Err("Expected x's scope to have a parent")
|
|
};
|
|
let parent_flags = semantic.scopes().get_flags(parent_scope_id);
|
|
if parent_flags.contains(ScopeFlags::Top) {
|
|
Ok(())
|
|
} else {
|
|
Err("Expected x to be in a top-level function declaration, but its parent scope has flags {parent_flags:?}")
|
|
}
|
|
})
|
|
.test();
|
|
tester.has_some_symbol("foo").is_not_in_scope(ScopeFlags::StrictMode).test();
|
|
}
|
|
|
|
#[test]
|
|
fn test_switch_case() {
|
|
SemanticTester::js(
|
|
"
|
|
const foo = 1;
|
|
switch (foo) {
|
|
case 1:
|
|
const foo = 2;
|
|
}
|
|
",
|
|
)
|
|
.has_root_symbol("foo")
|
|
.has_number_of_references(1)
|
|
.test();
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_parameters() {
|
|
let tester = SemanticTester::js(
|
|
"
|
|
const foo = 2;
|
|
const c = 0;
|
|
function func(a = foo, b = c, c = 0) {
|
|
const foo = 0;
|
|
}
|
|
",
|
|
);
|
|
|
|
tester.has_root_symbol("foo").has_number_of_references(1).test();
|
|
// b = c should reference the third parameter, so root symbol `c`` should have 0 reference
|
|
tester.has_root_symbol("c").has_number_of_references(0).test();
|
|
}
|
|
|
|
#[test]
|
|
fn test_catch_clause_parameters() {
|
|
SemanticTester::js(
|
|
"
|
|
const a = 0;
|
|
try {
|
|
} catch ({ [a]: b }) {
|
|
const a = 1;
|
|
}
|
|
",
|
|
)
|
|
.has_root_symbol("a")
|
|
.has_number_of_references(1)
|
|
.test();
|
|
}
|
|
|
|
#[test]
|
|
fn test_enums() {
|
|
let test = SemanticTester::ts(
|
|
"
|
|
enum A {
|
|
X,
|
|
Y,
|
|
Z
|
|
}",
|
|
);
|
|
test.has_root_symbol("A").contains_flags(SymbolFlags::RegularEnum).test();
|
|
test.has_some_symbol("X").contains_flags(SymbolFlags::EnumMember).test();
|
|
|
|
let semantic = test.build();
|
|
let program = semantic
|
|
.nodes()
|
|
.iter()
|
|
.find(|node| matches!(node.kind(), AstKind::Program(_)))
|
|
.expect("No program node found");
|
|
assert_eq!(program.scope_id(), semantic.scopes().root_scope_id());
|
|
|
|
let (enum_node, enum_decl) = semantic
|
|
.nodes()
|
|
.iter()
|
|
.find_map(|node| {
|
|
if let AstKind::TSEnumDeclaration(e) = node.kind() {
|
|
Some((node, e))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.expect("Expected TS test case to have an enum declaration for A.");
|
|
|
|
assert_eq!(
|
|
enum_node.scope_id(),
|
|
program.scope_id(),
|
|
"Expected `enum A` to be created in the top-level scope."
|
|
);
|
|
let enum_decl_scope_id = enum_decl.scope_id.get().expect("Enum declaration has no scope id");
|
|
assert_ne!(enum_node.scope_id(), enum_decl_scope_id, "Enum declaration nodes should contain the scope ID they create, not the scope ID they're created in.");
|
|
assert_eq!(enum_decl.members.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn var_hoisting() {
|
|
SemanticTester::js(
|
|
"
|
|
try {} catch (e) {
|
|
var e = 0;
|
|
}
|
|
",
|
|
)
|
|
.has_root_symbol("e")
|
|
// `e` was hoisted to the top scope so the symbol's scope is also the top scope
|
|
.is_in_scope(ScopeFlags::Top)
|
|
.test();
|
|
}
|