Skip to content

Conversation

@ikki-kki
Copy link

@ikki-kki ikki-kki commented Sep 16, 2025

Fixes false positives in no-unused-vars for non-PascalCase JSX components like CJK and camelCase identifiers.

Problem
The no-unused-vars rule incorrectly marks JSX components as "unused" when they use non-PascalCase naming, even when actually rendered in JSX. While variables and constants with multilingual characters or mixed casing are correctly recognized, component names like fooBar and 테스트 (CJK) trigger false positives.

This occurs because the parser only promotes JSX tags starting with uppercase letters, _, or $ to IdentifierReference, leaving others as plain Identifier nodes without generating semantic references. If this is intended to enforce the convention that "JSX components should start with uppercase letters," this should be handled by separate naming/JSX rules rather than having no-unused-vars, which is responsible for determining usage, issue warnings inappropriately.

Solution
Added a local fallback within the rule logic that checks for JSX usage when no semantic references exist
This maintains the existing heuristic for HTML intrinsics (pure lowercase ASCII) while correctly detecting usage for multilingual and mixed-case components.

Test:
Added test_jsx_plain_identifier_fallback covering CJK (테스트) and camelCase (fooBar) components, ensuring HTML intrinsics like <foo /> still trigger unused warnings.

oxlint eslint
스크린샷 2025-09-16 오후 1 51 51 스크린샷 2025-09-16 오후 1 51 20

Note: ESLint currently exhibits the same false positive behavior that this PR addresses for oxlint.

@ikki-kki ikki-kki requested a review from camc314 as a code owner September 16, 2025 08:43
Copilot AI review requested due to automatic review settings September 16, 2025 08:43
@graphite-app
Copy link
Contributor

graphite-app bot commented Sep 16, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@github-actions github-actions bot added A-linter Area - Linter C-bug Category - Bug labels Sep 16, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes false positives in the no-unused-vars rule for JSX components that use non-PascalCase naming conventions. The issue occurred because the parser only promotes JSX tags starting with uppercase letters, _, or $ to IdentifierReference, leaving multilingual (CJK) and camelCase component names as plain Identifier nodes without semantic references.

  • Added fallback logic to detect JSX usage for components with no semantic references
  • Maintained existing behavior for HTML intrinsics (pure lowercase ASCII) which should still trigger unused warnings
  • Added comprehensive test coverage for both CJK and camelCase component scenarios

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs Added JSX plain identifier fallback logic to detect usage of non-PascalCase components
crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs Added test cases covering CJK and camelCase components with proper pass/fail scenarios

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 201 to 219
for node in self.nodes().iter() {
match node.kind() {
AstKind::JSXOpeningElement(open) => {
if let JSXElementName::Identifier(ident) = &open.name {
if ident.name == target {
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
continue;
}
return true;
}
}
}
AstKind::JSXClosingElement(close) => {
if let JSXElementName::Identifier(ident) = &close.name {
if ident.name == target {
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
continue;
}
return true;
}
}
}
_ => {}
}
}
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logic for checking JSX element names is duplicated between opening and closing elements. Consider extracting this into a helper function to reduce code duplication.

Copilot uses AI. Check for mistakes.
Comment on lines 206 to 216
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
continue;
}
return true;
}
}
}
AstKind::JSXClosingElement(close) => {
if let JSXElementName::Identifier(ident) = &close.name {
if ident.name == target {
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic inconsistency bug: The code checks ident.name.chars().all(|c| c.is_ascii_lowercase()) in both JSXOpeningElement and JSXClosingElement branches, but this creates inconsistent behavior. If an opening tag like <foo> is skipped due to being all lowercase ASCII (treated as HTML intrinsic), but the closing tag </foo> doesn't exist or has different casing, the function could return true inappropriately. The logic should be consistent - either both opening and closing elements should use the same lowercase check, or the check should be done once before processing both element types to avoid inconsistent treatment of the same component name.

Suggested change
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
continue;
}
return true;
}
}
}
AstKind::JSXClosingElement(close) => {
if let JSXElementName::Identifier(ident) = &close.name {
if ident.name == target {
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
if ident.name.chars().all(|c| c.is_ascii_lowercase()) {
continue;
}
return true;
}
}
}
AstKind::JSXClosingElement(close) => {
if let JSXElementName::Identifier(ident) = &close.name {
if ident.name == target && !ident.name.chars().all(|c| c.is_ascii_lowercase()) {

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@ikki-kki ikki-kki force-pushed the fix/no-unused-vars-jsx-non-pascalcase-components branch from ae4bbbd to 2e4f620 Compare September 16, 2025 09:08
@camc314 camc314 self-assigned this Sep 16, 2025
Copy link
Contributor

@camc314 camc314 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work on this, and highlighting this issue.

However this isn't a bug in the linter, but rather the parser:

If we consider the following input to TS:

const myComponent = () => <div>Hello</div>;
const anotherComponent = () => <myComponent />;

It is transformed to:

const myComponent = () => React.createElement("div", null, "Hello");
const anotherComponent = () => React.createElement("myComponent", null);

Since both div and myComponent start with a lowercase letter, they are treated as intrinsic elements (HTML/SVG) and passed as string literals to React.createElement. Therefore, myComponent is not considered used in this context.

However, if we consider:

const 테스트 = () => <div>Hello</div>;
const anotherComponent = () => <테스트 />;

It is transformed to:

const 테스트 = () => React.createElement("div", null, "Hello");
const anotherComponent = () => React.createElement(테스트, null);

Here, 테스트 starts with a non-ASCII character, so it is treated as a custom component and references the JavaScript variable 테스트. Thus, 테스트 is considered used.

I've put up a pr #13819 to fix this.

Thanks

// ```
if self.is_in_jsx() && self.references().next().is_none() {
let target = self.name();
for node in self.nodes().iter() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performing this check is really slow as it requires looping over all all nodes in the ast. Are there other solutions?

@camc314 camc314 closed this Sep 16, 2025
graphite-app bot pushed a commit that referenced this pull request Sep 16, 2025
@ikki-kki
Copy link
Author

ikki-kki commented Sep 16, 2025

@camc314
Thank you for the response.

I saw that you fixed the parser issue in the PR you mentioned #13819
I had modified it to only affect within the lint rule scope because I was worried about potential side effects from touching the parser, but indeed fixing the parser was the right answer.

Glad the problem is resolved. I'll submit a PR if I encounter any bugs while using it.

Thanks

@camc314
Copy link
Contributor

camc314 commented Sep 16, 2025

no worries! thanks for contributing and using oxlint!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants