UNPKG

eslint-plugin-svelte

Version:
373 lines (372 loc) 16.2 kB
import { createRule } from '../utils/index.js'; import { getTypeScriptTools } from '../utils/ts-utils/index.js'; import { findVariable } from '../utils/ast-utils.js'; import { toRegExp } from '../utils/regexp.js'; let isRemovedWarningShown = false; export default createRule('no-unused-props', { meta: { docs: { description: 'Warns about defined Props properties that are unused', category: 'Best Practices', recommended: true }, schema: [ { type: 'object', properties: { checkImportedTypes: { type: 'boolean', default: false }, ignoreTypePatterns: { type: 'array', items: { type: 'string' }, default: [] }, ignorePropertyPatterns: { type: 'array', items: { type: 'string' }, default: [] }, allowUnusedNestedProperties: { type: 'boolean', default: false } }, additionalProperties: false } ], messages: { unusedProp: "'{{name}}' is an unused Props property.", unusedNestedProp: "'{{name}}' in '{{parent}}' is an unused property.", unusedIndexSignature: 'Index signature is unused. Consider using rest operator (...) to capture remaining properties.' }, type: 'suggestion', conditions: [ { svelteVersions: ['5'], runes: [true, 'undetermined'] } ] }, create(context) { const fileName = context.filename; const tools = getTypeScriptTools(context); if (!tools) { return {}; } const typeChecker = tools.service.program.getTypeChecker(); if (!typeChecker) { return {}; } const options = context.options[0] ?? {}; // TODO: Remove in v4 // MEMO: `ignorePatterns` was a property that only existed from v3.2.0 to v3.2.2. // From v3.3.0, it was replaced with `ignorePropertyPatterns` and `ignoreTypePatterns`. if (options.ignorePatterns != null && !isRemovedWarningShown) { console.warn('eslint-plugin-svelte: The `ignorePatterns` option in the `no-unused-props` rule has been removed. Please use `ignorePropertyPatterns` or/and `ignoreTypePatterns` instead.'); isRemovedWarningShown = true; } const checkImportedTypes = options.checkImportedTypes ?? false; const ignoreTypePatterns = (options.ignoreTypePatterns ?? []).map((p) => { if (typeof p === 'string') { return toRegExp(p); } return p; }); const ignorePropertyPatterns = (options.ignorePropertyPatterns ?? []).map((p) => { if (typeof p === 'string') { return toRegExp(p); } return p; }); function shouldIgnoreProperty(name) { return ignorePropertyPatterns.some((pattern) => pattern.test(name)); } function shouldIgnoreType(type) { function isMatched(name) { return ignoreTypePatterns.some((pattern) => pattern.test(name)); } const typeStr = typeChecker.typeToString(type); const symbol = type.getSymbol(); const symbolName = symbol?.getName(); return isMatched(typeStr) || (symbolName ? isMatched(symbolName) : false); } function isInternalProperty(symbol) { const declarations = symbol.getDeclarations(); if (!declarations || declarations.length === 0) return false; return declarations.every((decl) => decl.getSourceFile().fileName === fileName); } /** * Extracts property paths from member expressions. */ function getPropertyPath(node) { const paths = []; let currentNode = node; let parentNode = currentNode.parent ?? null; while (parentNode) { if (parentNode.type === 'MemberExpression' && parentNode.object === currentNode) { const property = parentNode.property; if (property.type === 'Identifier') { paths.push(property.name); } else if (property.type === 'Literal' && typeof property.value === 'string') { paths.push(property.value); } else { break; } } currentNode = parentNode; parentNode = currentNode.parent ?? null; } return paths; } /** * Finds all property access paths for a given variable. */ function getUsedNestedPropertyPathsArray(node) { const variable = findVariable(context, node); if (!variable) return []; const pathsArray = []; for (const reference of variable.references) { if ('identifier' in reference && reference.identifier.type === 'Identifier' && (reference.identifier.range[0] !== node.range[0] || reference.identifier.range[1] !== node.range[1])) { const referencePath = getPropertyPath(reference.identifier); pathsArray.push(referencePath); } } return pathsArray; } /** * Checks if a property is from TypeScript's built-in type definitions. * These properties should be ignored as they are not user-defined props. */ function isBuiltInProperty(prop) { const declarations = prop.getDeclarations(); if (!declarations || declarations.length === 0) return false; const declaration = declarations[0]; const sourceFile = declaration.getSourceFile(); if (!sourceFile) return false; return sourceFile.fileName.includes('node_modules/typescript/lib/'); } function getUsedPropertyNamesFromPattern(pattern) { const usedProps = new Set(); for (const prop of pattern.properties) { if (prop.type === 'Property') { if (prop.key.type === 'Identifier') { usedProps.add({ originalName: prop.key.name, aliasName: prop.key.name }); } else if (prop.key.type === 'Literal' && typeof prop.key.value === 'string' && prop.value.type === 'Identifier') { usedProps.add({ originalName: prop.key.value, aliasName: prop.value.name }); } } else if (prop.type === 'RestElement') { // If there's a rest element, all properties are potentially used return new Set(); } } return usedProps; } /** * Check if the type is a class type (has constructor or prototype) */ function isClassType(type) { if (!type) return false; // Check if it's a class instance type if (type.isClass()) return true; // Check for constructor signatures const constructorType = type.getConstructSignatures(); if (constructorType.length > 0) return true; // Check if it has a prototype property const symbol = type.getSymbol(); if (symbol?.members?.has('prototype')) return true; return false; } /** * Recursively checks for unused properties in a type. */ function checkUnusedProperties({ propsType, usedPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }) { // Skip checking if the type itself is a class if (isClassType(propsType)) return; const typeStr = typeChecker.typeToString(propsType); if (checkedPropsTypes.has(typeStr)) return; checkedPropsTypes.add(typeStr); if (shouldIgnoreType(propsType)) return; const properties = typeChecker.getPropertiesOfType(propsType); const propsBaseTypes = propsType.getBaseTypes(); if (!properties.length && (!propsBaseTypes || propsBaseTypes.length === 0)) { return; } if (propsBaseTypes) { for (const propsBaseType of propsBaseTypes) { checkUnusedProperties({ propsType: propsBaseType, usedPropertyPaths, declaredPropertyNames, reportNode, parentPath, checkedPropsTypes, reportedPropertyPaths }); } } for (const prop of properties) { if (isBuiltInProperty(prop)) continue; if (!checkImportedTypes && !isInternalProperty(prop)) continue; const propName = prop.getName(); if (shouldIgnoreProperty(propName)) continue; const currentPath = [...parentPath, propName]; const currentPathStr = [...parentPath, propName].join('.'); if (reportedPropertyPaths.has(currentPathStr)) continue; const propType = typeChecker.getTypeOfSymbol(prop); const isUsedThisInPath = usedPropertyPaths.includes(currentPathStr); const isUsedInPath = usedPropertyPaths.some((path) => { return path.startsWith(`${currentPathStr}.`); }); if (isUsedThisInPath && !isUsedInPath) { continue; } const isUsedInProps = Array.from(declaredPropertyNames).some((p) => { return p.originalName === propName; }); if (!isUsedInPath && !isUsedInProps) { reportedPropertyPaths.add(currentPathStr); context.report({ node: reportNode, messageId: parentPath.length ? 'unusedNestedProp' : 'unusedProp', data: { name: propName, parent: parentPath.join('.') } }); continue; } const isUsedNested = usedPropertyPaths.some((path) => { return path.startsWith(`${currentPathStr}.`); }); if (isUsedNested || isUsedInProps) { checkUnusedProperties({ propsType: propType, usedPropertyPaths, declaredPropertyNames, reportNode, parentPath: currentPath, checkedPropsTypes, reportedPropertyPaths }); } } // Check for unused index signatures only at the root level if (parentPath.length === 0) { const indexType = propsType.getStringIndexType(); const numberIndexType = propsType.getNumberIndexType(); const hasIndexSignature = Boolean(indexType) || Boolean(numberIndexType); if (hasIndexSignature && !hasRestElement(declaredPropertyNames)) { context.report({ node: reportNode, messageId: 'unusedIndexSignature' }); } } } /** * Returns true if the destructuring pattern includes a rest element, * which means all remaining properties are potentially used. */ function hasRestElement(declaredPropertyNames) { return declaredPropertyNames.size === 0; } function normalizeUsedPaths(paths, allowUnusedNestedProperties) { const normalized = []; for (const path of paths.sort((a, b) => a.length - b.length)) { if (path.length === 0) continue; if (normalized.some((p) => p.every((part, idx) => part === path[idx]))) { continue; } normalized.push(path); } return normalized.map((path) => { // If we allow unused nested properties, only return first level properties if (allowUnusedNestedProperties) return [path[0]]; return path; }); } return { 'VariableDeclaration > VariableDeclarator': (node) => { // Only check $props declarations if (node.init?.type !== 'CallExpression' || node.init.callee.type !== 'Identifier' || node.init.callee.name !== '$props') { return; } const tsNode = tools.service.esTreeNodeToTSNodeMap.get(node); if (!tsNode || !tsNode.type) return; const propsType = typeChecker.getTypeFromTypeNode(tsNode.type); let usedPropertyPathsArray = []; let declaredPropertyNames = new Set(); if (node.id.type === 'ObjectPattern') { declaredPropertyNames = getUsedPropertyNamesFromPattern(node.id); if (declaredPropertyNames.size === 0) return; const identifiers = []; for (const p of node.id.properties) { if (p.type !== 'Property') { continue; } if (p.value.type === 'Identifier') { identifiers.push(p.value); } else if (p.value.type === 'AssignmentPattern' && p.value.left.type === 'Identifier') { identifiers.push(p.value.left); } } for (const identifier of identifiers) { const paths = getUsedNestedPropertyPathsArray(identifier); usedPropertyPathsArray.push(...paths.map((path) => [identifier.name, ...path])); } } else if (node.id.type === 'Identifier') { usedPropertyPathsArray = getUsedNestedPropertyPathsArray(node.id); } checkUnusedProperties({ propsType, usedPropertyPaths: normalizeUsedPaths(usedPropertyPathsArray, options.allowUnusedNestedProperties).map((pathArray) => { return pathArray.join('.'); }), declaredPropertyNames, reportNode: node.id, parentPath: [], checkedPropsTypes: new Set(), reportedPropertyPaths: new Set() }); } }; } });