Skip to content

Commit 49d031e

Browse files
Rel1cxRel1cx
authored andcommitted
Add enhanced FQN helper and replace internal key check
1 parent f51d445 commit 49d031e

4 files changed

Lines changed: 180 additions & 118 deletions

File tree

packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
33
import { ESLintUtils } from "@typescript-eslint/utils";
44
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
55
import { unionConstituents } from "ts-api-utils";
6-
import ts from "typescript";
76

8-
import { createRule } from "../utils";
7+
import { createRule, getFullyQualifiedNameEx } from "../utils";
98

109
export const RULE_NAME = "no-implicit-key";
1110

@@ -43,7 +42,7 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
4342
if (key == null) return;
4443
// Allow pass-through of React internally defined keys
4544
// https://github.com/Rel1cx/eslint-react/issues/1472
46-
if (isReactInternalKey(checker, key)) return;
45+
if (getFullyQualifiedNameEx(checker, key).endsWith("React.Attributes.key")) return;
4746
context.report({
4847
messageId: "default",
4948
node,
@@ -52,21 +51,3 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
5251
},
5352
};
5453
}
55-
56-
/**
57-
* Check if a symbol is a React internal key
58-
* @param checker TypeScript type checker
59-
* @param key Symbol to check
60-
* @returns True if the symbol is a React internal key, false otherwise
61-
*/
62-
function isReactInternalKey(checker: ts.TypeChecker, key: ts.Symbol) {
63-
if (checker.getFullyQualifiedName(key) === "React.Attributes.key") return true;
64-
let parent = key.declarations?.at(0)?.parent;
65-
while (parent != null && parent.kind !== ts.SyntaxKind.SourceFile) {
66-
if (ts.isModuleDeclaration(parent) && ts.isIdentifier(parent.name) && parent.name.text === "React") {
67-
return true;
68-
}
69-
parent = parent.parent;
70-
}
71-
return false;
72-
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./create-rule";
22
export * from "./type-is";
3+
export * from "./type-name";
34
export * from "./type-variant";
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import ts from "typescript";
2+
3+
/**
4+
* An enhanced version of getFullyQualifiedName that handles cases that original function does not handle
5+
* @param checker TypeScript type checker
6+
* @param symbol Symbol to get fully qualified name for
7+
* @returns Fully qualified name of the symbol
8+
*/
9+
export function getFullyQualifiedNameEx(checker: ts.TypeChecker, symbol: ts.Symbol) {
10+
let name = symbol.name;
11+
let parent = symbol.declarations?.at(0)?.parent;
12+
if (parent == null) return checker.getFullyQualifiedName(symbol);
13+
while (parent.kind !== ts.SyntaxKind.SourceFile) {
14+
switch (true) {
15+
case ts.isInterfaceDeclaration(parent):
16+
name = `${parent.name.text}.${name}`;
17+
break;
18+
case ts.isTypeAliasDeclaration(parent):
19+
name = `${parent.name.text}.${name}`;
20+
break;
21+
case ts.isEnumDeclaration(parent):
22+
name = `${parent.name.text}.${name}`;
23+
break;
24+
case ts.isClassDeclaration(parent)
25+
&& parent.name != null:
26+
name = `${parent.name.text}.${name}`;
27+
break;
28+
case ts.isModuleDeclaration(parent):
29+
name = `${parent.name.text}.${name}`;
30+
break;
31+
case ts.isNamespaceImport(parent):
32+
name = `${parent.name.text}.${name}`;
33+
break;
34+
case ts.isNamespaceExport(parent):
35+
name = `${parent.name.text}.${name}`;
36+
break;
37+
case ts.isEnumMember(parent):
38+
name = `${parent.name.getText()}.${name}`;
39+
break;
40+
case ts.isFunctionDeclaration(parent)
41+
&& parent.name != null:
42+
name = `${parent.name.text}.${name}`;
43+
break;
44+
case ts.isClassExpression(parent)
45+
&& parent.name != null:
46+
name = `${parent.name.text}.${name}`;
47+
break;
48+
case ts.isPropertySignature(parent)
49+
&& ts.isIdentifier(parent.name):
50+
name = `${parent.name.text}.${name}`;
51+
break;
52+
case ts.isPropertyDeclaration(parent)
53+
&& ts.isIdentifier(parent.name):
54+
name = `${parent.name.text}.${name}`;
55+
break;
56+
case ts.isMethodDeclaration(parent)
57+
&& ts.isIdentifier(parent.name):
58+
name = `${parent.name.text}.${name}`;
59+
break;
60+
case ts.isMethodSignature(parent)
61+
&& ts.isIdentifier(parent.name):
62+
name = `${parent.name.text}.${name}`;
63+
break;
64+
case ts.isPropertyAssignment(parent)
65+
&& ts.isIdentifier(parent.name):
66+
name = `${parent.name.text}.${name}`;
67+
break;
68+
// Skip unnamed structural type nodes (they don't contribute a name segment)
69+
case ts.isTypeLiteralNode(parent):
70+
case ts.isMappedTypeNode(parent):
71+
case ts.isObjectLiteralExpression(parent):
72+
case ts.isIntersectionTypeNode(parent):
73+
case ts.isUnionTypeNode(parent):
74+
break;
75+
}
76+
parent = parent.parent;
77+
}
78+
return name;
79+
}

0 commit comments

Comments
 (0)