UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

324 lines (323 loc) 12.8 kB
"use strict"; /** * Utility for detecting magic numbers in TypeScript/JavaScript code */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.detectMagicNumbers = detectMagicNumbers; const typescript_1 = __importDefault(require("typescript")); const path = __importStar(require("path")); const constants_1 = require("../constants"); /** * Detects magic numbers in a TypeScript source file * @param sourceFile The TypeScript source file * @param filePath The full path to the file * @param getContextCode Function to get context lines around a node * @returns Array of detected magic numbers */ function detectMagicNumbers(sourceFile, filePath, getContextCode) { const magicNumbers = []; const fileName = path.basename(filePath); function visit(node) { // Check if the node is a numeric literal if (typescript_1.default.isNumericLiteral(node)) { const value = parseFloat(node.text); // Skip common numbers, algorithm constants, and color constants if (constants_1.COMMON_NUMBERS.has(value) || constants_1.ALGORITHM_CONSTANTS.has(value) || constants_1.COLOR_CONSTANTS.has(value)) { typescript_1.default.forEachChild(node, visit); return; } // Skip numbers in specific contexts that are likely not magic numbers if (isInAcceptableContext(node, sourceFile)) { typescript_1.default.forEachChild(node, visit); return; } // Get line number (1-based for display) const lineNumber = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; // Get context around the magic number const context = getContextCode(node, sourceFile); magicNumbers.push({ value, line: lineNumber, fileName, filePath, context: { before: context.before, current: context.line, after: context.after, }, }); } typescript_1.default.forEachChild(node, visit); } visit(sourceFile); return magicNumbers; } /** * Checks if a numeric literal is in an acceptable context where it's likely not a magic number * @param node The numeric literal node * @param sourceFile The source file for additional context * @returns True if the number is in an acceptable context */ function isInAcceptableContext(node, sourceFile) { const parent = node.parent; if (!parent) { return false; } // Array index access: arr[0], arr[1] if (typescript_1.default.isElementAccessExpression(parent) && parent.argumentExpression === node) { return true; } // Array literal with simple indices: [1, 2, 3] if (typescript_1.default.isArrayLiteralExpression(parent)) { return true; } // Property access with numeric key: obj[123] if (typescript_1.default.isComputedPropertyName(parent)) { return true; } // Enum values if (typescript_1.default.isEnumMember(parent)) { return true; } // Version numbers in strings or template literals (parent context) if (typescript_1.default.isTemplateSpan(parent) || typescript_1.default.isStringLiteral(parent)) { return true; } // Default parameter values with small numbers if (typescript_1.default.isParameter(parent) && parseFloat(node.text) <= 10) { return true; } // Port numbers (typically 3000-9999 range for development) const value = parseFloat(node.text); if (value >= 3000 && value <= 9999) { return true; } // CSS-related values - check if in a property assignment that looks like CSS if (typescript_1.default.isPropertyAssignment(parent)) { const propertyName = getPropertyName(parent); if (propertyName && constants_1.CSS_PROPERTIES.has(propertyName)) { return true; } } // Object literal values that might be CSS or business data if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isObjectLiteralExpression(parent.parent)) { const propertyName = getPropertyName(parent); if (propertyName && (constants_1.CSS_PROPERTIES.has(propertyName) || constants_1.BUSINESS_DATA_PATTERNS.some((pattern) => pattern.test(propertyName)) || constants_1.LAYOUT_CALCULATION_PATTERNS.some((pattern) => pattern.test(propertyName)))) { return true; } } // Constant declarations (const, let with descriptive names) if (typescript_1.default.isVariableDeclaration(parent)) { const variableName = parent.name; if (typescript_1.default.isIdentifier(variableName)) { // Check if it's a constant-style name const name = variableName.text; if (isConstantStyleName(name)) { return true; } } } // Variable declarations with const keyword if (typescript_1.default.isVariableDeclaration(parent) && parent.parent && typescript_1.default.isVariableDeclarationList(parent.parent)) { const declarationList = parent.parent; if (declarationList.flags & typescript_1.default.NodeFlags.Const) { return true; } } // JSX attribute values (React component props) if (typescript_1.default.isJsxExpression(parent) && typescript_1.default.isJsxAttribute(parent.parent)) { const attributeName = parent.parent.name; if (typescript_1.default.isIdentifier(attributeName)) { const propName = attributeName.text; if (constants_1.UI_PROPS.has(propName) || constants_1.CHART_PROPS.has(propName) || constants_1.ANIMATION_PATTERNS.some((pattern) => pattern.test(propName))) { return true; } } } // Array constructor for UI repetition: Array(n) if (typescript_1.default.isCallExpression(parent) && typescript_1.default.isIdentifier(parent.expression)) { if (parent.expression.text === "Array") { return true; } } // Function call arguments that are likely UI-related if (typescript_1.default.isCallExpression(parent)) { const functionName = getFunctionName(parent); if (functionName && isUIRelatedFunction(functionName)) { return true; } } // Check for animation/motion context if (isInAnimationContext(node)) { return true; } // Check context patterns in the surrounding code if (hasAcceptableContext(node, sourceFile)) { return true; } // Check for default parameter values if (isDefaultParameterValue(node)) { return true; } return false; } /** * Gets the property name from a property assignment * @param propertyAssignment The property assignment node * @returns The property name as string or null */ function getPropertyName(propertyAssignment) { const name = propertyAssignment.name; if (typescript_1.default.isIdentifier(name)) { return name.text; } if (typescript_1.default.isStringLiteral(name)) { return name.text; } return null; } /** * Checks if a variable name follows constant naming conventions * @param name The variable name to check * @returns True if it looks like a constant name */ function isConstantStyleName(name) { return constants_1.CONSTANT_PATTERNS.some((pattern) => pattern.test(name)); } /** * Gets the function name from a call expression * @param callExpression The call expression node * @returns The function name or null */ function getFunctionName(callExpression) { const expression = callExpression.expression; if (typescript_1.default.isIdentifier(expression)) { return expression.text; } if (typescript_1.default.isPropertyAccessExpression(expression)) { // Handle both obj.method() and Math.sin() patterns if (typescript_1.default.isIdentifier(expression.expression) && typescript_1.default.isIdentifier(expression.name)) { return `${expression.expression.text}.${expression.name.text}`; } if (typescript_1.default.isIdentifier(expression.name)) { return expression.name.text; } } return null; } /** * Checks if a function name is UI-related * @param functionName The function name to check * @returns True if it's likely a UI-related function */ function isUIRelatedFunction(functionName) { return constants_1.UI_FUNCTIONS.has(functionName); } /** * Checks if a numeric literal is in an animation/motion context * @param node The numeric literal node * @returns True if it's in an animation context */ function isInAnimationContext(node) { let currentNode = node; // Walk up the AST to find animation-related contexts while (currentNode.parent) { currentNode = currentNode.parent; // Check for object literals with animation properties if (typescript_1.default.isObjectLiteralExpression(currentNode)) { const properties = currentNode.properties; return properties.some((prop) => { if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name)) { const propName = prop.name.text; return constants_1.ANIMATION_PATTERNS.some((pattern) => pattern.test(propName)); } return false; }); } // Check for function calls with animation-related names if (typescript_1.default.isCallExpression(currentNode)) { const functionName = getFunctionName(currentNode); if (functionName && (functionName.includes("animate") || functionName.includes("transition") || functionName.includes("motion") || functionName.includes("spring"))) { return true; } } } return false; } /** * Checks if the context around the number suggests it's not a magic number * @param node The numeric literal node * @param sourceFile The source file for context * @returns True if context suggests it's acceptable */ function hasAcceptableContext(node, sourceFile) { const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); const fileLines = sourceFile.text.split("\n"); const currentLine = fileLines[line] || ""; // Check if the line matches any context patterns return constants_1.CONTEXT_PATTERNS.some((pattern) => pattern.test(currentLine)); } /** * Checks if a numeric literal is a default parameter value * @param node The numeric literal node * @returns True if it's a default parameter assignment */ function isDefaultParameterValue(node) { const parent = node.parent; // Check for function parameter defaults: function(param = 123) if (typescript_1.default.isParameter(parent) && parent.initializer === node) { return true; } // Check for destructuring defaults: { prop = 123 } if (typescript_1.default.isBindingElement(parent) && parent.initializer === node) { return true; } // Check for property assignment with equals (default props) if (typescript_1.default.isBinaryExpression(parent) && parent.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken) { return true; } return false; }