Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "refactor: optimize identifier tracking in enforce-use-client rule",
"packageName": "@fluentui/eslint-plugin-react-components",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,6 @@ export const rule = createRule<[RuleOptions?], MessageIds>({
* Checks if an identifier references an imported custom hook or RSC-unsafe function
* and records it as a client feature if found.
*
* This is separated from the ImportDeclaration visitor for performance:
* - ImportDeclaration builds O(n) lookup sets during import scanning
* - This helper performs O(1) set membership checks per identifier
*
* Alternative approach of merging with scope-based analysis would require
* O(scope depth) traversal per identifier, which is significantly slower.
*
* @param node - The identifier node to check
* @param name - The identifier name
*/
Expand Down Expand Up @@ -275,70 +268,70 @@ export const rule = createRule<[RuleOptions?], MessageIds>({
},

/**
* Track imported custom hooks and RSC-unsafe functions
* Track imports and detect when imported custom hooks or RSC-unsafe functions are referenced
*/
ImportDeclaration(node: TSESTree.ImportDeclaration) {
Identifier(node: TSESTree.Identifier) {
if (shouldSkipAnalysis()) {
return;
}

// Defensive null check to satisfy TypeScript
// In practice, identifiers in the AST should always have a parent node
const parent = node.parent;
if (!parent) {
return;
}

// Track React default/namespace imports (import React from 'react', import * as React from 'react')
const source = node.source.value;
if (source === 'react') {
for (const specifier of node.specifiers) {
if (
specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier ||
specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier
) {
ruleState.reactImportNames.add(specifier.local.name);
}
if (
parent.type === AST_NODE_TYPES.ImportDefaultSpecifier ||
parent.type === AST_NODE_TYPES.ImportNamespaceSpecifier
) {
const importDecl = parent.parent as TSESTree.ImportDeclaration;
if (importDecl?.source?.value === 'react') {
ruleState.reactImportNames.add(node.name);
}
return;
}

for (const specifier of node.specifiers) {
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
const importedName =
specifier.imported.type === AST_NODE_TYPES.Identifier
? specifier.imported.name
: specifier.imported.value;
// Track imported custom hooks and RSC-unsafe functions
if (parent.type === AST_NODE_TYPES.ImportSpecifier) {
let importedName: string;
let localName: string;

// Track custom hooks
if (isPotentialCustomHook(importedName)) {
ruleState.importedCustomHooks.add(specifier.local.name);
if (parent.imported.type === AST_NODE_TYPES.Identifier) {
// Normal import: { useFoo } or { useFoo as bar }
// Process only when visiting the imported identifier to avoid duplicate processing
if (parent.imported !== node) {
return; // Skip when visiting the local alias
}

// Track RSC-unsafe functions (including user-configured ones)
if (rscUnsafeFunctions.has(importedName)) {
ruleState.importedRSCUnsafeFunctions.add(specifier.local.name);
importedName = node.name;
localName = parent.local.name;
} else {
// String literal import: { "use-foo" as bar }
// The imported node is a Literal (not visited by Identifier), so process on local identifier
if (parent.local !== node) {
return;
}
importedName = (parent.imported as TSESTree.Literal).value as string;
localName = node.name;
}
}
},

/**
* Detect when imported custom hooks or RSC-unsafe functions are referenced
*/
Identifier(node: TSESTree.Identifier) {
if (shouldSkipAnalysis()) {
return;
}

// Defensive null check to satisfy TypeScript
// In practice, identifiers in the AST should always have a parent node
const parent = node.parent;
if (!parent) {
if (isPotentialCustomHook(importedName)) {
ruleState.importedCustomHooks.add(localName);
}
if (rscUnsafeFunctions.has(importedName)) {
ruleState.importedRSCUnsafeFunctions.add(localName);
}
return;
}

// Skip type annotations, type parameters, and import/export declarations
// Skip type annotations, type parameters, and export declarations
if (
parent.type === AST_NODE_TYPES.TSTypeReference ||
parent.type === AST_NODE_TYPES.TSTypeQuery ||
parent.type === AST_NODE_TYPES.TSTypeAnnotation ||
parent.type === AST_NODE_TYPES.TSTypeParameter ||
parent.type === AST_NODE_TYPES.ImportSpecifier ||
parent.type === AST_NODE_TYPES.ImportDefaultSpecifier ||
parent.type === AST_NODE_TYPES.ImportNamespaceSpecifier ||
parent.type === AST_NODE_TYPES.ExportSpecifier
) {
return;
Expand Down