sicua
Version:
A tool for analyzing project structure and dependencies
324 lines (323 loc) • 12.8 kB
JavaScript
"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;
}