frontend-standards-checker
Version:
A comprehensive frontend standards validation tool with TypeScript support
1,126 lines • 53.9 kB
JavaScript
"use strict";
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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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.checkInlineStyles = checkInlineStyles;
exports.checkCommentedCode = checkCommentedCode;
exports.checkHardcodedData = checkHardcodedData;
exports.checkFunctionComments = checkFunctionComments;
exports.checkUnusedVariables = checkUnusedVariables;
exports.checkFunctionNaming = checkFunctionNaming;
exports.checkInterfaceNaming = checkInterfaceNaming;
exports.checkStyleConventions = checkStyleConventions;
exports.checkEnumsOutsideTypes = checkEnumsOutsideTypes;
exports.checkHookFileExtension = checkHookFileExtension;
exports.checkAssetNaming = checkAssetNaming;
exports.checkNamingConventions = checkNamingConventions;
exports.checkDirectoryNaming = checkDirectoryNaming;
exports.checkComponentStructure = checkComponentStructure;
exports.checkComponentFunctionNameMatch = checkComponentFunctionNameMatch;
exports.checkTypesSeparation = checkTypesSeparation;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const acorn = __importStar(require("acorn"));
const acornWalk = __importStar(require("acorn-walk"));
const file_scanner_js_1 = require("../utils/file-scanner.js");
const index_js_1 = require("../helpers/index.js");
/**
* Additional validators for code quality and conventions.
*
* This module provides a set of functions to validate code style, naming conventions,
* directory structure, and content rules for TypeScript/JavaScript projects. It includes
* checks for inline styles, commented code, hardcoded data, function and interface naming,
* unused variables, asset naming, directory naming, and component structure.
*
* @packageDocumentation
*/
// Naming conventions by file type
const NAMING_RULES = [
{
dir: 'components',
regex: /^[A-Z][a-zA-Z0-9]+\.tsx$/,
desc: 'Components must be in PascalCase and end with .tsx',
},
{
dir: 'hooks',
regex: /^use[A-Z][a-zA-Z0-9]*\.hook\.(ts|tsx)$/,
desc: 'Hooks must start with use followed by PascalCase and end with .hook.ts or .hook.tsx',
},
{
dir: 'constants',
regex: /^[a-z][a-zA-Z0-9]*\.constant\.ts$/,
desc: 'Constants must be camelCase and end with .constant.ts',
},
{
dir: 'helper',
regex: /^[a-z][a-zA-Z0-9]*\.helper\.ts$/,
desc: 'Helpers must be camelCase and end with .helper.ts',
},
{
dir: 'helpers',
regex: /^[a-z][a-zA-Z0-9]*\.helper\.ts$/,
desc: 'Helpers must be camelCase and end with .helper.ts',
},
{
dir: 'types',
regex: /^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)*\.type\.ts$/,
desc: 'Types must be camelCase and end with .type.ts (may include additional extensions like .provider.type.ts)',
},
{
dir: 'styles',
regex: /^[a-z][a-zA-Z0-9]*\.style\.ts$/,
desc: 'Styles must be camelCase and end with .style.ts',
},
{
dir: 'enums',
regex: /^[a-z][a-zA-Z0-9]*\.enum\.ts$/,
desc: 'Enums must be camelCase and end with .enum.ts',
},
{
dir: 'assets',
regex: /^[a-z0-9]+(-[a-z0-9]+)*\.(svg|png|jpg|jpeg|gif|webp|ico)$/,
desc: 'Assets must be in kebab-case (e.g., service-error.svg)',
},
];
// Keep track of flagged directories to avoid duplicate reports
const flaggedDirectories = new Set();
/**
* Check for inline styles
*/
function checkInlineStyles(content, filePath) {
const lines = content.split('\n');
const errors = [];
// Detect if this is a React Native project
const isRNProject = (0, file_scanner_js_1.isReactNativeProject)(filePath);
// For React Native projects, be more permissive with SVG components
if (isRNProject) {
// Skip inline style validation for SVG components in React Native
if (filePath.includes('/assets/Svg/') ||
filePath.includes('/Svg/') ||
filePath.includes('.svg')) {
return errors;
}
}
lines.forEach((line, idx) => {
if (/style\s*=\s*\{\{[^}]*\}\}/.test(line)) {
errors.push({
rule: 'No inline styles',
message: 'Avoid inline styles, use CSS classes or styled components',
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'style',
});
}
});
return errors;
}
/**
* Check for commented code
*/
function checkCommentedCode(content, filePath) {
const lines = content.split('\n');
const errors = [];
let inJSDoc = false;
let inMultiLineComment = false;
lines.forEach((line, idx) => {
const trimmedLine = line.trim();
// Track JSDoc comment state
if (/^\s*\/\*\*/.test(line)) {
inJSDoc = true;
return;
}
if (inJSDoc && /\*\//.test(line)) {
inJSDoc = false;
return;
}
if (/^\s*\/\*/.test(line) && !/^\s*\/\*\*/.test(line)) {
inMultiLineComment = true;
return;
}
if (inMultiLineComment && /\*\//.test(line)) {
inMultiLineComment = false;
return;
}
// Skip if we're inside any comment block
if (inJSDoc || inMultiLineComment || /^\s*\*/.test(line)) {
return;
}
// Check for single-line comments that might be commented code
if (/^\s*\/\//.test(line)) {
// Skip if it's a valid comment (not commented code)
const commentContent = trimmedLine.replace(/^\/\/\s*/, '');
// Skip common valid comment patterns
if (
// ESLint/TSLint directives
/eslint|tslint|@ts-|prettier/.test(line) ||
// Task comments
/^(TODO|FIXME|NOTE|HACK|BUG|XXX):/i.test(commentContent) ||
// Documentation comments
/^(This|The|When|If|For|To|Used)/.test(commentContent) ||
/^(Returns?|Handles?|Checks?|Sets?|Gets?)/.test(commentContent) ||
// Explanation comments
/because|since|due to|in order to|to ensure|to avoid|to prevent|explanation|reason/i.test(commentContent) ||
// Configuration comments
/config|setting|option|parameter|default|override/i.test(commentContent) ||
// Single word or very short explanatory comments
/^[A-Z][a-z]*(\s+[a-z]+){0,3}\.?$/.test(commentContent) ||
// Comments with common English sentence patterns
/^(and|or|but|however|therefore|thus|also|additionally)/.test(commentContent) ||
// Comments that end with periods (likely explanations)
commentContent.trim().endsWith('.test') ||
// Comments that are clearly explanatory
(commentContent.length > 50 && !/^[a-z]+\(/.test(commentContent))) {
return;
}
// Check if it looks like commented code
const looksLikeCode =
// Function calls
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/.test(commentContent) ||
// Variable assignments
/^(const|let|var|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/.test(commentContent) ||
// Return statements
/^return\s+/.test(commentContent) ||
// Import/export statements
/^(import|export)\s+/.test(commentContent) ||
// Object/array syntax
/^[{[].*}]$/.test(commentContent) ||
// Console statements
/^console\.[a-z]+\s*\(/.test(commentContent) ||
// Control flow statements with parentheses
/^(if|for|while|switch|try|catch)\s*\(/.test(commentContent);
if (looksLikeCode) {
errors.push({
rule: 'Commented code',
message: 'Leaving commented code in the repository is not allowed.',
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'content',
});
}
}
});
return errors;
}
/**
* Check for hardcoded data
*/
function checkHardcodedData(content, filePath) {
const lines = content.split('\n');
const errors = [];
// Skip configuration, setup, helper, config, and constants files entirely
if ((0, index_js_1.isConfigOrConstantsFile)(filePath)) {
return errors;
}
// Detect if this is a React Native project
const isRNProject = (0, file_scanner_js_1.isReactNativeProject)(filePath);
// For React Native projects, be more permissive with SVG components and assets
if (isRNProject) {
// Skip SVG components and asset files in React Native projects
if (filePath.includes('/assets/') ||
filePath.includes('/Svg/') ||
filePath.includes('.svg') ||
filePath.includes('/constants/') ||
filePath.includes('/config/')) {
return errors;
}
}
if (filePath.includes('jest.setup.ts')) {
return errors;
}
// Track JSDoc comment blocks
let inJSDocComment = false;
lines.forEach((line, idx) => {
// Track JSDoc comment state
if (/^\s*\/\*\*/.test(line)) {
inJSDocComment = true;
}
if (inJSDocComment && /\*\//.test(line)) {
inJSDocComment = false;
return; // Skip this line as it ends the JSDoc
}
// Skip if we're inside a JSDoc comment
if (inJSDocComment || /^\s*\*/.test(line)) {
return;
}
// Check for hardcoded data but exclude CSS classes, Tailwind classes, and other valid cases
const hasHardcodedPattern = /(['"])[^'"\n]{0,200}?(\d{3,}|lorem|dummy|test|prueba|foo|bar|baz)[^'"\n]{0,200}?\1/.test(line);
// Check for Spanish hardcoded text patterns (user-facing text)
const hasSpanishHardcodedText = /(['"])[A-ZÁÉÍÓÚÑÜ][a-záéíóúñü\s]{8,}[a-záéíóúñüA-ZÁÉÍÓÚÑÜ]['"]/i.test(line) &&
!/^[A-Z_][A-Z0-9_]*$/.test(line.match(/['"]([^'"]+)['"]/)?.[1] || '') &&
!/(className|class|style|import|from|href|src|id|name|type|role|aria-|data-|testid|test-id)/i.test(line);
const isCSSClass = /className\s*=|class\s*=|style\s*=/.test(line);
// Comprehensive Tailwind CSS pattern matching (include classes with brackets)
const tailwindPatterns = [
/\b[pmwh]-\d+\b/,
/\b(text|bg|border|rounded|shadow)-\d+\b/,
/\b(grid|flex|space|gap)-\d+\b/,
/\b(top|bottom|left|right|inset)-\d+\b/,
/\b(font|leading|tracking|opacity)-\d+\b/,
/\b(scale|rotate|translate)-\d+\b/,
/\b(cursor|select)-\d+\b/,
/\b(transition|duration|ease)-\d+\b/,
/\b(hover|focus|active|disabled)-\d+\b/,
/\b(absolute|relative|fixed|static|sticky|block|inline|hidden|visible)-\d+\b/,
/\b([smd]|lg|xl|2xl):/,
/\b(text|bg|border)-([a-z]+)-(50|100|200|300|400|500|600|700|800|900|950)\b/,
/\b(from|via|to|ring|outline|divide|decoration)-([a-z]+)-(50|100|200|300|400|500|600|700|800|900|950)\b/,
/\b(text|bg|border)-(semantic|custom|brand|primary|secondary|accent|success|warning|error|info|muted|disabled)-[a-z]+-(?:[5-9]0|[1-9]00|950)\b/,
/\b(text|bg|border)-[a-z]+-[a-z]*-?\d{2,3}\b/,
/[a-z-]{1,40}-\[[^\]\n]{1,100}\]/,
];
const isTailwindClass = tailwindPatterns.some((pattern) => pattern.test(line));
const isTestFile = /mock|__test__|\.test\.|\.spec\./.test(filePath);
const isImportStatement = /import.*from/.test(line.trim());
const isExportStatement = /export\s+\{[^}]*\}\s+from/.test(line.trim());
const isImportFrom = /\bfrom\s+['"][^'"]+['"]/.test(line.trim());
if (line
.trim()
.includes('@Containers/Transfers/screens/CardLessDebitWithdrawal/screens/Personalization/__test__/mocks')) {
console.log(line.trim());
console.log(isImportFrom);
}
const isURL = /https?:\/.\//.test(line);
const isSingleLineComment = /^\s*\/\//.test(line);
const isMultiLineComment = /^\s*\/\*/.test(line) && /\*\//.test(line);
// Additional check: if line contains common CSS/Tailwind context
const hasClassContext = /(className|class)\s*[:=]\s*['"`]/.test(line) ||
/['"`]\s*\?\s*['"`][^'"`\n]{0,50}\d+[^'"`\n]{0,50}['"`]\s*:\s*['"`]/.test(line);
// Robust translation key detection: allow for whitespace, any key, and any translation function (t, useTranslations, i18n, etc.)
const isTranslationAssignment = /:\s*t\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/=\s*t\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/:\s*useTranslations\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/=\s*useTranslations\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/:\s*i18n\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/=\s*i18n\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
// Remote texts and configuration keys
/useRemoteTexts\s*<?[^>]*>?\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/getTexts?\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line) ||
/getText?\s*\(\s*['"`][^'"`]+['"`]\s*\)/.test(line);
// Check for valid configuration contexts that should not be flagged as hardcoded data
const isValidConfiguration = /(weight|subsets|style|display)\s*:\s*\[/.test(line) ||
(/weight\s*:\s*\[/.test(content.substring(Math.max(0, content.indexOf(line) - 200), content.indexOf(line) + 100)) &&
/['"]\d{3}['"]/.test(line)) ||
/\b(timeout|port|delay|duration|interval|retry|maxRetries|limit|size|width|height|fontSize|lineHeight)\s*:\s*['"]?\d+['"]?/.test(line) ||
/['"](\d+\.){1,2}\d+['"]/.test(line) ||
/['"]\/api\/v\d+\//.test(line) ||
/(from|to|via|offset|opacity|scale|rotate|skew|translate)\s*:\s*['"][\d-]+['"]/.test(line) ||
/(fontSize|spacing|borderRadius|colors)\s*:\s*\{/.test(line) ||
/\b(useTranslations|t)\s*\(\s*['"][a-zA-Z]+(\.[a-zA-Z]+)*['"]/.test(line) ||
/\b(toast|notification)\.(success|error|info|warning)\s*\(\s*t\s*\(/.test(line) ||
/['"][a-zA-Z]+(\.[a-zA-Z]+){2,}['"]/.test(line) ||
// Configuration keys for remote texts, themes, contexts
(/['"][A-Z][A-Za-z0-9]*['"]/.test(line) &&
/useRemoteTexts|getTexts?|getText|useTheme|getTheme|useContext|getContext/.test(line));
// Check for UI component props that are legitimate configuration values
const isUIComponentProp = /\b(rounded|radius|width|height|size|margin|padding|spacing|gap|flex|opacity|zIndex|elevation|borderWidth|shadowOffset|shadowOpacity|shadowRadius|blurRadius)\s*=\s*['"]?\d+['"]?/.test(line) ||
/\b(top|bottom|left|right|start|end)\s*=\s*['"]?\d+['"]?/.test(line) ||
/\b(rows|cols|span|order|weight)\s*=\s*['"]?\d+['"]?/.test(line);
// Check for skeleton/loading component files that contain legitimate UI configuration
const isSkeletonComponent = /[Ss]keleton/.test(filePath) ||
/[Ll]oading/.test(filePath) ||
/[Pp]laceholder/.test(filePath);
const isConfigurationFile = /\/(config|configs|constants|theme|styles|fonts)\//.test(filePath) ||
/\.(config|constants|theme|styles|fonts)\.(ts|tsx|js|jsx)$/.test(filePath) ||
/\/fonts\//.test(filePath);
// Skip property accessor patterns like Colors['color-complementary-cyan-500']
const isPropertyAccessor = /\w{1,40}\s*\[\s*['"][^'"]{1,100}['"]\s*\]/.test(line);
// Skip technical identifiers (icon names, property checks, route names, etc.)
const isTechnicalIdentifier =
// Icon names (common icon libraries)
/iconName\s*=\s*['"][a-z-]+['"]/.test(line) ||
/\bname\s*[:=]\s*['"][a-z-]+['"]/.test(line) ||
// Property existence checks using 'in' operator
/['"][a-zA-Z]+['"]\s+in\s+\w+/.test(line) ||
// Route names and navigation identifiers in TypeScript generics
/extends\s+RouteProp<\{[^}]*['"][A-Za-z]+['"][^}]*\}/.test(line) ||
/RouteProp<\s*\{\s*[A-Za-z]+\s*:\s*[^}]*\}\s*,\s*['"][A-Za-z]+['"]/.test(line) ||
/navigate\(\s*\w+\.\w+\s*\)/.test(line) ||
// Object property names in TypeScript generics
/<\s*{\s*[A-Za-z]+:\s*[^}]*}\s*>/.test(line) ||
// Method calls with property names
/getCardDetail\(\s*['"][a-zA-Z]+['"]\s*\)/.test(line) ||
/\.\w+\(\s*['"][a-zA-Z]+['"]\s*\)/.test(line) ||
// Configuration keys for Redux, AsyncStorage, etc.
/key\s*:\s*['"][a-zA-Z]+['"]/.test(line) ||
/name\s*:\s*['"][a-zA-Z]+['"]/.test(line) ||
// Store/slice configuration
/createSlice\s*\(\s*\{[^}]*name\s*:\s*['"][a-zA-Z]+['"]/.test(content.substring(Math.max(0, content.indexOf(line) - 100), content.indexOf(line) + 100));
// Skip legitimate method calls that might contain trigger words
const isLegitimateMethodCall = /\.(endsWith|startsWith|includes|indexOf|replace|match|test)\s*\(\s*['"][^'"]*\b(test|dummy|foo|bar|baz)\b[^'"]*['"]\s*\)/.test(line) ||
/\.(contains|equals|equalsIgnoreCase)\s*\(\s*['"][^'"]*\b(test|dummy|foo|bar|baz)\b[^'"]*['"]\s*\)/.test(line);
// Skip system/API state comparisons and enum-like values
const isSystemStateComparison =
// Permission states, app states, platform states
/\s*(===?|!==?)\s*['"](?:authorized|denied|granted|limited|restricted|undetermined|blocked)['"]/.test(line) ||
/\s*(===?|!==?)\s*['"](?:active|background|inactive|foreground|unknown)['"]/.test(line) ||
// Platform identifiers
/\s*(===?|!==?)\s*['"](?:ios|android|web|windows|macos|linux)['"]/.test(line) ||
// Common system status values
/\s*(===?|!==?)\s*['"](?:success|error|pending|loading|idle|complete)['"]/.test(line) ||
// Network states
/\s*(===?|!==?)\s*['"](?:online|offline|connected|disconnected)['"]/.test(line) ||
// File/media states
/\s*(===?|!==?)\s*['"](?:available|unavailable|ready|not-ready)['"]/.test(line);
// Skip legitimate configuration values and common patterns
const isLegitimateConfiguration =
// TypeScript/acorn configuration values
/ecmaVersion\s*:\s*['"]latest['"]/.test(line) ||
// Testing directory names
/===?\s*['"]__tests?__['"]/.test(line) ||
/currentDirName\s*===?\s*['"]__tests?__['"]/.test(line) ||
// Rule validation messages (not actual usage)
/message\s*:\s*['"][^'"]*\balert\(\)[^'"]*['"]/.test(line) ||
/Issue\s*:\s*['"][^'"]*\balert\(\)[^'"]*['"]/.test(line) ||
// Common legitimate string comparisons
/\.\s*includes\s*\(\s*['"][^'"]*\b(test|dummy|foo|bar|baz)\b[^'"]*['"]\s*\)/.test(line) ||
// Directory and file name checks
/\s*(===?|!==?)\s*['"][^'"]*\b(__tests?__|__mocks?__|test|spec)\b[^'"]*['"]/.test(line) ||
// Configuration arrays (ignorePatterns, defaultIgnorePatterns, etc.)
/\b(default)?[Ii]gnorePatterns?\s*=\s*\[/.test(content) ||
/\b(exclude|ignore|skip)Patterns?\s*:\s*\[/.test(content) ||
// Array elements for configuration patterns
(/^\s*['"][^'"]*['"],?\s*$/.test(line.trim()) &&
/\b(default)?[Ii]gnorePatterns?\s*=\s*\[/m.test(content)) ||
// Enum and constant comparisons
(/\s*(===?|!==?)\s*['"][a-z-]+['"]/.test(line) &&
!/[A-Z]/.test(line.match(/['"]([^'"]+)['"]/)?.[1] || '')) ||
// App state and permission values
/\s*(===?|!==?)\s*['"](?:authorized|denied|granted|active|background|inactive|foreground)['"]/.test(line) ||
// Platform and system values
/\s*(===?|!==?)\s*['"](?:ios|android|web|windows|macos|linux)['"]/.test(line) ||
// Return enum values (permission results, status codes, etc.)
/return\s+\w+\s*===?\s*['"][a-z-]+['"]/.test(line) ||
/:\s*\w+Enum\.\w+/.test(line);
if ((hasHardcodedPattern || hasSpanishHardcodedText) &&
!isCSSClass &&
!isTailwindClass &&
!hasClassContext &&
!isTestFile &&
!isImportStatement &&
!isImportFrom &&
!isExportStatement &&
!isURL &&
!isSingleLineComment &&
!isMultiLineComment &&
!isValidConfiguration &&
!isConfigurationFile &&
!isTranslationAssignment &&
!isPropertyAccessor &&
!isLegitimateMethodCall &&
!isLegitimateConfiguration &&
!isUIComponentProp &&
!isSkeletonComponent &&
!isSystemStateComparison &&
!isTechnicalIdentifier) {
errors.push({
rule: 'Hardcoded data',
message: 'No hardcoded data should be left in the code except in mocks.',
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'content',
});
}
});
return errors;
}
/**
* Check for missing comments in complex functions
* This function analyzes JavaScript/TypeScript code to identify functions with high complexity
* that lack proper documentation. It calculates complexity based on control flow structures,
* async operations, array methods, and function length. Functions exceeding complexity
* thresholds require explanatory comments or JSDoc documentation.
*/
function checkFunctionComments(content, filePath) {
const lines = content.split('\n');
const errors = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line)
continue;
const trimmedLine = line.trim();
if ((0, index_js_1.shouldSkipLine)(trimmedLine))
continue;
const functionMatch = (0, index_js_1.detectFunctionDeclaration)(trimmedLine);
if (!functionMatch)
continue;
const functionName = (0, index_js_1.getFunctionName)(functionMatch);
if (!functionName || (0, index_js_1.shouldSkipFunction)(trimmedLine, functionName))
continue;
const functionAnalysis = (0, index_js_1.analyzeFunctionComplexity)(lines, i, content);
if (!functionAnalysis.isComplex)
continue;
if (!(0, index_js_1.hasProperComments)(lines, i, content)) {
errors.push((0, index_js_1.createCommentError)(functionName, functionAnalysis, filePath, i));
}
}
return errors;
}
/**
* Check for unused variables
*/
function checkUnusedVariables(content, filePath) {
const errors = [];
try {
// Enable location tracking to get line numbers
const ast = acorn.parse(content, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true, // Important for line numbers
});
const declared = new Map(); // name -> { node, exported: false }
const used = new Set();
const exportedViaSpecifier = new Set();
// Find `export { foo }` and `export default foo`
acornWalk.simple(ast, {
ExportNamedDeclaration(node) {
const exportNode = node;
if (exportNode.specifiers && Array.isArray(exportNode.specifiers)) {
for (const specifier of exportNode.specifiers) {
if (specifier &&
typeof specifier === 'object' &&
'local' in specifier &&
specifier.local &&
'name' in specifier.local) {
exportedViaSpecifier.add(specifier.local.name);
}
}
}
},
ExportDefaultDeclaration(node) {
const exportNode = node;
if (exportNode.declaration &&
'name' in exportNode.declaration &&
exportNode.declaration.name) {
exportedViaSpecifier.add(exportNode.declaration.name);
}
},
});
// Pass 1: Find all declarations and mark if they are exported.
acornWalk.ancestor(ast, {
VariableDeclarator(node, ancestors) {
const declNode = node;
if (declNode.id &&
declNode.id.type === 'Identifier' &&
'name' in declNode.id &&
declNode.id.name) {
const name = declNode.id.name;
const parent = ancestors[ancestors.length - 2];
const grandParent = ancestors.length > 2
? ancestors[ancestors.length - 3]
: null;
const isInlineExport = parent &&
parent.type === 'VariableDeclaration' &&
grandParent &&
grandParent.type === 'ExportNamedDeclaration';
const isSpecifierExport = exportedViaSpecifier.has(name);
if (!declared.has(name)) {
declared.set(name, {
node: declNode.id,
exported: Boolean(isInlineExport ?? isSpecifierExport),
});
}
}
},
});
// Pass 2: Find all usages.
acornWalk.ancestor(ast, {
Identifier(node, ancestors) {
const identNode = node;
const parent = ancestors[ancestors.length - 2];
if (!('name' in identNode) || !identNode.name) {
return;
}
const isDeclaration = (parent &&
parent.type === 'VariableDeclarator' &&
parent.id === identNode) ||
(parent &&
parent.type === 'FunctionDeclaration' &&
parent.id === identNode) ||
(parent &&
parent.type === 'ClassDeclaration' &&
parent.id === identNode) ||
(parent &&
parent.type === 'ImportSpecifier' &&
parent.local === identNode) ||
(parent &&
parent.type === 'ImportDefaultSpecifier' &&
parent.local === identNode) ||
(parent &&
parent.type === 'ImportNamespaceSpecifier' &&
parent.local === identNode) ||
(parent &&
(parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression') &&
parent.params &&
Array.isArray(parent.params) &&
parent.params.includes(identNode));
const isPropertyKey = parent &&
parent.type === 'Property' &&
parent.key === identNode &&
!('computed' in parent && parent.computed);
if (!isDeclaration && !isPropertyKey) {
used.add(identNode.name);
}
},
});
for (const [name, decl] of declared.entries()) {
if (!used.has(name) && !decl.exported && !name.startsWith('_')) {
const nodeWithLoc = decl.node;
const lineNumber = nodeWithLoc?.loc?.start?.line ?? 1;
errors.push({
rule: 'No unused variables',
message: `Variable '${name}' is declared but never used. (@typescript-eslint/no-unused-vars rule)`,
filePath: filePath,
line: lineNumber,
severity: 'warning',
category: 'content',
});
}
}
}
catch (error) {
// If acorn fails to parse (e.g., complex TS syntax), we skip silently
// This is expected for complex TypeScript syntax that acorn can't handle
console.debug(`Could not parse ${filePath} for unused variable analysis: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
return errors;
}
/**
* Check for function naming conventions
*/
function checkFunctionNaming(content, filePath) {
const errors = [];
const lines = content.split('\n');
lines.forEach((line, idx) => {
// Match function declarations
const functionDeclMatch = line.match(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
// Match arrow function assignments
const arrowFuncMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s*)?\(/g);
const matches = [];
if (functionDeclMatch)
matches.push(...functionDeclMatch);
if (arrowFuncMatch)
matches.push(...arrowFuncMatch);
matches.forEach((match) => {
const nameMatch = /(?:function\s+|const\s+|let\s+|var\s+)([a-zA-Z_$][a-zA-Z0-9_$]*)/.exec(match);
if (!nameMatch?.[1]) {
return;
}
const functionName = nameMatch[1];
// Skip React components (PascalCase) and hooks (start with 'use')
if (/^[A-Z]/.test(functionName) || functionName.startsWith('use')) {
return;
}
// Function should follow camelCase
if (!/^[a-z][a-zA-Z0-9]*$/.test(functionName)) {
errors.push({
rule: 'Function naming',
message: 'Functions must follow camelCase convention (e.g., getProvinces)',
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'naming',
});
}
});
});
return errors;
}
/**
* Check for interface naming conventions
*/
function checkInterfaceNaming(content, filePath) {
const errors = [];
const lines = content.split('\n');
const seen = new Set();
lines.forEach((line, idx) => {
const interfaceMatch = line.match(/export\s+interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
if (interfaceMatch) {
interfaceMatch.forEach((match) => {
const nameMatch = /export\s+interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/.exec(match);
if (!nameMatch?.[1]) {
return;
}
const interfaceName = nameMatch[1];
if (seen.has(interfaceName))
return;
seen.add(interfaceName);
if (!/^I[A-Z][a-zA-Z0-9]*$/.test(interfaceName)) {
errors.push({
rule: 'Interface naming',
message: 'Exported interfaces must start with "I" and follow PascalCase (e.g., IButtonProps)',
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'naming',
});
}
});
}
});
// Patch: Suppress "Type naming" and "No any type" in .d.ts files
return errors;
}
/**
* Check for style conventions
*/
function checkStyleConventions(content, filePath) {
const errors = [];
// Only check .style.ts files
if (!filePath.endsWith('.style.ts')) {
return errors;
}
const lines = content.split('\n');
lines.forEach((line, idx) => {
// Check if StyleSheet.create is used (for React Native)
if (/StyleSheet\.create\s*\(/.test(line)) {
// This is good, StyleSheet.create is being used
return;
}
// Check for style object exports
const exportMatch = /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/.exec(line);
if (exportMatch?.[1]) {
const styleName = exportMatch[1];
// Style names should be in camelCase and end with 'Styles'
if (!/^[a-z][a-zA-Z0-9]*Styles$/.test(styleName)) {
errors.push({
rule: 'Style naming',
message: `Style object '${styleName}' should be in camelCase and end with 'Styles' (e.g., cardPreviewStyles)`,
filePath: filePath,
line: idx + 1,
severity: 'error',
category: 'naming',
});
}
}
});
return errors;
}
/**
* Check for enums outside types folder
*/
function checkEnumsOutsideTypes(filePath) {
// Check if enum files are incorrectly placed inside types directory
if (filePath.includes('types') && filePath.endsWith('.enum.ts')) {
return {
rule: 'Enum outside of types',
message: 'Enums must be in a separate directory from types (use /enums/ instead of /types/).',
filePath: filePath,
severity: 'error',
category: 'structure',
};
}
return null;
}
/**
* Check for hook file extension
*/
function checkHookFileExtension(filePath) {
// Only check for hooks (use*.hook.ts[x]?)
const fileName = path_1.default.basename(filePath);
const dirName = path_1.default.dirname(filePath);
if (!/^use[a-zA-Z0-9]+\.hook\.(ts|tsx)$/.test(fileName))
return null;
// Omit if index.ts in the same folder
if (fs_1.default.existsSync(path_1.default.join(dirName, 'index.ts')))
return null;
try {
const content = fs_1.default.readFileSync(filePath, 'utf8');
// Heuristic: if contains JSX (return < or React.createElement), must be .tsx
const needsRender = /return\s*<|React\.createElement/.test(content);
const isTSX = fileName.endsWith('.tsx');
if (needsRender && !isTSX) {
return {
rule: 'Hook file extension',
message: 'Hooks that render JSX must have a .tsx extension.',
filePath: filePath,
severity: 'error',
category: 'naming',
};
}
if (!needsRender && isTSX) {
return {
rule: 'Hook file extension',
message: 'Hooks that do not render JSX should have a .ts extension.',
filePath: filePath,
severity: 'error',
category: 'naming',
};
}
}
catch {
// If we can't read the file, skip validation silently
return null;
}
return null;
}
/**
* Check for asset naming conventions
*/
function checkAssetNaming(filePath) {
const fileName = path_1.default.basename(filePath);
const fileExt = path_1.default.extname(fileName);
const baseName = fileName.replace(fileExt, '');
// Check if file is in assets directory
if (!filePath.includes('/assets/') && !filePath.includes('\\assets\\')) {
return null;
}
// Skip TypeScript declaration files (.d.ts)
if (fileExt === '.ts' && fileName.endsWith('.d.ts')) {
return null;
}
// Detect if this is a React Native project
const isRNProject = (0, file_scanner_js_1.isReactNativeProject)(filePath);
// For React Native projects, be more permissive with SVG components
if (isRNProject) {
// Skip asset naming validation for SVG components in React Native
if (filePath.includes('/Svg/') ||
filePath.includes('/assets/Svg/') ||
fileExt === '.tsx' ||
fileExt === '.jsx') {
return null;
}
}
// Assets should follow kebab-case
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(baseName)) {
return {
rule: 'Asset naming',
message: 'Assets must follow kebab-case convention (e.g., service-error.svg)',
filePath: filePath,
severity: 'error',
category: 'naming',
};
}
return null;
}
/**
* Check naming conventions for files
*/
function checkNamingConventions(filePath) {
const rel = filePath.split(path_1.default.sep);
const fname = rel[rel.length - 1];
const parentDir = rel[rel.length - 2]; // Get immediate parent directory
// Omit index.tsx and index.ts from naming convention checks
if (fname === 'index.tsx' || fname === 'index.ts') {
return null;
}
if (!fname || !parentDir) {
return null;
}
for (const rule of NAMING_RULES) {
// Check if the immediate parent directory matches the rule directory
if (parentDir === rule.dir) {
if (!rule.regex.test(fname)) {
return {
rule: 'Naming',
message: rule.desc,
filePath: filePath,
severity: 'error',
category: 'naming',
};
}
}
}
return null;
}
/**
* Check directory naming conventions
*/
function checkDirectoryNaming(dirPath) {
const errors = [];
const currentDirName = path_1.default.basename(dirPath);
// Skip excluded directories and files
if (currentDirName.startsWith('.') ||
currentDirName === 'node_modules' ||
currentDirName === '__tests__' ||
currentDirName === '__test__' ||
currentDirName.includes('(') ||
currentDirName.includes(')') ||
currentDirName.includes('[') ||
currentDirName.includes(']') ||
currentDirName === 'coverage' ||
currentDirName === 'dist' ||
currentDirName === 'build' ||
currentDirName === 'public' ||
currentDirName === 'static' ||
currentDirName === 'temp' ||
currentDirName === 'tmp' ||
currentDirName.startsWith('__') ||
// Skip framework-specific directories that are allowed to have different naming
currentDirName === 'api' ||
currentDirName === 'lib' ||
currentDirName === 'utils' ||
currentDirName === 'pages' ||
currentDirName === 'components' ||
currentDirName === 'styles' ||
currentDirName === 'types' ||
currentDirName === 'hooks' ||
currentDirName === 'constants' ||
currentDirName === 'helpers' ||
currentDirName === 'assets' ||
currentDirName === 'enums') {
return errors;
}
// Skip if it's the root directory or first level directories in monorepo (apps, packages, etc)
if (['apps', 'packages', 'config', 'k8s', 'src'].includes(currentDirName)) {
return errors;
}
// Only check directories that are actually inside meaningful project structure
const ROOT_DIR = process.cwd();
const relativePath = path_1.default.relative(ROOT_DIR, dirPath);
if (relativePath.includes('/src/') &&
!relativePath.includes('/node_modules/')) {
// Check if current directory follows camelCase convention
// Allow PascalCase for component directories and camelCase for others
const isCamelCase = /^[a-z][a-zA-Z0-9]*$/.test(currentDirName);
const isPascalCase = /^[A-Z][a-zA-Z0-9]*$/.test(currentDirName);
// Special case: Allow kebab-case for route directories (Next.js routing)
const isValidRouteDir = /^[a-z0-9]+(-[a-z0-9]+)*$/.test(currentDirName) &&
(relativePath.includes('/app/') || relativePath.includes('/pages/'));
if (!isCamelCase && !isPascalCase && !isValidRouteDir) {
// Check if we've already flagged this directory name in any path
const alreadyFlagged = Array.from(flaggedDirectories).some((flaggedPath) => path_1.default.basename(flaggedPath) === currentDirName);
if (!alreadyFlagged) {
// Generate a proper camelCase suggestion based on the actual directory name
const camelCaseSuggestion = currentDirName
.toLowerCase()
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
errors.push({
rule: 'Directory naming',
message: `Directory '${currentDirName}' should follow camelCase convention (e.g., '${camelCaseSuggestion}')`,
filePath: dirPath,
severity: 'error',
category: 'naming',
});
// Mark this directory name as flagged
flaggedDirectories.add(dirPath);
}
}
}
return errors;
}
/**
* Helper function to check index file requirements
*/
function checkIndexFileRequirements(componentDir, componentName, isUtilityDir) {
const errors = [];
const indexTsxFile = path_1.default.join(componentDir, 'index.tsx');
const indexTsFile = path_1.default.join(componentDir, 'index.ts');
if (isUtilityDir) {
// Utility directories should have index.ts
if (!fs_1.default.existsSync(indexTsFile)) {
errors.push({
rule: 'Component structure',
message: `Utility directory '${componentName}' should have an index.ts file for exports`,
filePath: indexTsFile,
severity: 'warning',
category: 'structure',
});
}
}
else if (!fs_1.default.existsSync(indexTsxFile) && !fs_1.default.existsSync(indexTsFile)) {
// Component directories should have index.tsx or index.ts
errors.push({
rule: 'Component structure',
message: 'Component must have an index.tsx file (for components) or index.ts file (for exports)',
filePath: indexTsxFile,
severity: 'warning',
category: 'structure',
});
}
return errors;
}
/**
* Helper function to check hooks directory structure
*/
function checkHooksDirectory(componentDir) {
const errors = [];
const hooksDir = path_1.default.join(componentDir, 'hooks');
if (fs_1.default.existsSync(hooksDir)) {
try {
const hookFiles = fs_1.default
.readdirSync(hooksDir)
.filter((file) => file.endsWith('.hook.ts') || file.endsWith('.hook.tsx'));
if (hookFiles.length === 0) {
errors.push({
rule: 'Component structure',
message: 'Hooks directory should contain hook files with .hook.ts or .hook.tsx extension',
filePath: hooksDir,
severity: 'warning',
category: 'structure',
});
}
}
catch {
// Directory access error - skip this check
}
}
return errors;
}
/**
* Helper function to check type file naming in types directory
*/
function checkTypesDirectory(componentDir) {
const errors = [];
const typesDir = path_1.default.join(componentDir, 'types');
if (fs_1.default.existsSync(typesDir)) {
try {
const typeFiles = fs_1.default
.readdirSync(typesDir)
.filter((file) => file.endsWith('.ts') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
file !== 'index.ts');
for (const typeFile of typeFiles) {
// Exception: if .d.ts and in type or types folder, do not require .type.ts
const isDeclaration = typeFile.endsWith('.d.ts');
const parentDir = path_1.default.basename(typesDir).toLowerCase();
if (isDeclaration && (parentDir === 'type' || parentDir === 'types')) {
continue;
}
if (!typeFile.endsWith('.type.ts')) {
const typeFilePath = path_1.default.join(typesDir, typeFile);
errors.push({
rule: 'Component type naming',
message: 'Type file should end with .type.ts',
filePath: typeFilePath,
severity: 'error',
category: 'naming',
});
}
}
}
catch {
// Directory access error - skip this check
}
}
return errors;
}
/**
* Helper function to check style file naming in styles directory
*/
function checkStylesDirectory(componentDir) {
const errors = [];
const stylesDir = path_1.default.join(componentDir, 'styles');
if (fs_1.default.existsSync(stylesDir)) {
try {
const styleFiles = fs_1.default
.readdirSync(stylesDir)
.filter((file) => file.endsWith('.ts') &&
!file.includes('.test.') &&
!file.includes('.spec.'));
for (const styleFile of styleFiles) {
if (!styleFile.endsWith('.style.ts')) {
const styleFilePath = path_1.default.join(stylesDir, styleFile);
errors.push({
rule: 'Component style naming',
message: 'Style file should end with .style.ts',
filePath: styleFilePath,
severity: 'error',
category: 'naming',
});
}
}
}
catch {
// Directory access error - skip this check
}
}
return errors;
}
/**
* Check component structure
*/
function checkComponentStructure(componentDir) {
const errors = [];
const componentName = path_1.default.basename(componentDir);
// Skip validation for generic 'components' directories that are just containers
if (componentName === 'components') {
return errors;
}
// Different expectations based on directory type
const isUtilityDir = [
'hooks',
'types',
'constants',
'helpers',
'utils',
].includes(componentName);
// Check index file requirements
errors.push(...checkIndexFileRequirements(componentDir, componentName, isUtilityDir));
// Check subdirectories
errors.push(...checkHooksDirectory(componentDir));
errors.push(...checkTypesDirectory(componentDir));
errors.push(...checkStylesDirectory(componentDir));
return errors;
}
/**
* Check component function name matches folder name
*/
function checkComponentFunctionNameMatch(content, filePath) {
if (!(0, index_js_1.shouldProcessFile)(filePath)) {
return null;
}
// Find the first PascalCase directory from the file upwards
const dirName = (0, index_js_1.findPascalCaseDirectory)(filePath);
const isPascalCase = /^[A-Z][a-zA-Z0-9]*$/.test(dirName);
// Check if it's a valid anonymous export (should pass validation)
const hasValidAnonymousExport = /export\s+default\s+\([^)]*\)\s*=>\s*/.test(content) ||
/export\s+default\s+<[^>]*>\s*\([^)]*\)\s*=>\s*/.test(content) ||
/export\s+default\s+(memo|forwardRef)\s*\(/.test(content);
if (hasValidAnonymousExport) {
return null; // Allow anonymous exports
}
const functionMatch = (0, index_js_1.findFunctionMatch)(content);
if (!functionMatch) {
return (0, index_js_1.createNoFunctionError)(dirName, filePath);
}
const { functionName, lineNumber } = functionMatch;
return (0, index_js_1.validateFunctionName)(functionName, dirName, isPascalCase, filePath, lineNumber);
}
/**
* Check if interfaces, types, enums or constants are defined in separate files
* This ensures proper separation of concerns and better code organization
*/
function checkTypesSeparation(content, filePath) {
const errors = [];
const lines = content.spli