@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
604 lines (506 loc) • 19.6 kB
JavaScript
/**
* C003_no_vague_abbreviations - Enhanced Regex-based Rule Analyzer
* Category: coding
*
* Detects vague variable names and unclear abbreviations
* Uses enhanced regex patterns with proper comment filtering
*/
const fs = require('fs');
const path = require('path');
const { CommentDetector } = require('../../utils/rule-helpers');
class C003NoVagueAbbreviations {
constructor(options = {}) {
this.options = {
allowedSingleChar: new Set(options.allowedSingleChar || [
'i', 'j', 'k', 'x', 'y', 'z', 'n', 'm', 't', 'v', 'r', 'e', 'p', 'w', 'h'
]),
allowedAbbreviations: new Set(options.allowedAbbreviations || [
// Technical abbreviations from user feedback
'id', 'url', 'uri', 'api', 'ui', 'db', 'fs', 'os', 'io', 'ai', 'ml', 'qa', 'ci', 'cd', 'pr',
'jwt', 'uuid', 'json', 'xml', 'html', 'css', 'sql', 'http', 'https', 'ftp', 'smtp', 'tcp', 'udp',
'pdf', 'csv', 'tsv', 'png', 'jpg', 'gif', 'svg', 'mp4', 'mp3', 'zip', 'tar',
'js', 'ts', 'py', 'rb', 'go', 'rs', 'kt', 'cs', 'vb', 'sh',
'dom', 'xhr', 'spa', 'pwa', 'seo', 'cdn', 'ssl', 'tls',
'orm', 'ddl', 'dml', 'etl', 'olap', 'oltp',
'kpi', 'roi', 'sla', 'poc', 'mvp', 'b2b', 'b2c', 'crm', 'erp',
'jsx', 'tsx', 'vue', 'scss', 'less',
'it', 'ut', 'e2e',
// Common development terms
'config', 'env', 'app', 'btn', 'img', 'src', 'dest', 'req', 'res', 'ctx',
'min', 'max', 'len', 'num', 'str', 'auth', 'log', 'err', 'msg', 'key',
// Add the variants from user feedback cases
'qa1', 'ci1', 'tsx2', 'it2', 'qa2', 'ci2',
// Common test/function context terms
'value', 'result', 'response', 'request', 'data', 'item', 'element', 'object',
// Common programming terms
'async', 'length', 'ms'
]),
minLength: options.minLength || 2,
strictMode: options.strictMode || false
};
// Patterns for suspicious abbreviations
this.suspiciousPatterns = [
/^[a-z]{1,2}[0-9]*$/, // e.g., 'u', 'usr', 'n1', 'v2'
/^[a-z]*[aeiou]*[bcdfghjklmnpqrstvwxyz]{4,}$/, // too many consonants
/^(tmp|temp|val|var|data|info|item|elem|el|obj|arr)([A-Z0-9].*)?$/, // generic names
];
// Unclear names that should be avoided in most contexts (but not in specific contexts)
this.unclearNames = new Set([
'stuff', 'thing', 'something', 'anything', 'everything',
'flag', 'check', 'test', 'validate', 'process', 'handle',
'obj', 'arg', 'val', 'fn'
// Removed: 'data', 'info', 'element', 'object', 'value', 'result', 'response', 'request', 'temp', 'tmp', 'variable'
// These are often acceptable in specific contexts
]);
// Context patterns where single letters are acceptable
this.loopPatterns = [
/for\s*\(\s*(?:let|const|var)\s+([a-z])\s*[=;]/i,
/\.forEach\s*\(\s*(?:\([^)]*\)|\w+)\s*=>/,
/\.map\s*\(\s*(?:\([^)]*\)|\w+)\s*=>/,
/\.filter\s*\(\s*(?:\([^)]*\)|\w+)\s*=>/
];
}
/**
* Check if variable is in loop context
*/
isLoopContext(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
const prevLine = lines[line - 2] || '';
const nextLine = lines[line] || '';
const contextLines = [prevLine, currentLine, nextLine].join(' ');
// Check for for loops
if (/for\s*\(\s*(?:let|const|var)\s+\w+/i.test(contextLines)) {
return true;
}
// Check for array methods
if (/\.(forEach|map|filter|reduce|some|every|find|findIndex)\s*\(/i.test(contextLines)) {
return true;
}
return false;
}
/**
* Check if variable is a generic type parameter
*/
isGenericTypeParameter(content, variableName, line) {
// Single uppercase letters T, U, V, K, etc. are common generic type names
if (!/^[T-Z]$/i.test(variableName)) {
return false;
}
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
const prevLine = lines[line - 2] || '';
const nextLine = lines[line] || '';
const contextLines = [prevLine, currentLine, nextLine].join(' ');
// Check for generic type context
const genericPatterns = [
/function\s*<[^>]*>/i, // function<T, U>
/class\s+\w+\s*<[^>]*>/i, // class Foo<T>
/interface\s+\w+\s*<[^>]*>/i, // interface Bar<T>
/type\s+\w+\s*<[^>]*>/i, // type Baz<T>
/<[^>]*>.*=>/i, // <T>(param: T) =>
/:\s*[T-Z]\s*[,)]/i // parameter: T,
];
return genericPatterns.some(pattern => pattern.test(contextLines));
}
/**
* Check if variable is a TypeScript type annotation context
*/
isTypeAnnotationContext(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
// Check if variable is in type position: param: Type, variable: Type
if (/:\s*\w+\s*[,)=]/.test(currentLine)) {
return true;
}
return false;
}
/**
* Check if variable is function parameter context
*/
isFunctionParameter(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
// Check if we're in a function parameter list
const functionPatterns = [
/function.*\(.*\w+.*\)/, // function foo(param)
/\(.*\w+.*\)\s*=>/, // (param) =>
/\w+\s*=>\s*/, // param =>
/\w+\s*:\s*\w+\s*[,)]/, // param: Type,
/\w+\?\s*:\s*\w+\s*[,)]/ // param?: Type,
];
return functionPatterns.some(pattern => pattern.test(currentLine));
}
/**
* Check if variable has clear type information
*/
hasTypeInformation(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
// TypeScript type annotations make variables clearer
// Example: function process(u: User) - 'u' is clear from User type
const typePatterns = [
new RegExp(`${variableName}\\s*:\\s*[A-Z]\\w+`), // param: TypeName
new RegExp(`${variableName}\\s*:\\s*\\w+\\[\\]`), // param: Type[]
new RegExp(`${variableName}\\s*:\\s*string|number|boolean`) // primitive types
];
return typePatterns.some(pattern => pattern.test(currentLine));
}
/**
* Check if variable is coordinate-like or math notation
*/
isCoordinate(variableName) {
return /^[xyz](\d+)?$/i.test(variableName) ||
/^(width|height|top|left|right|bottom)$/i.test(variableName) ||
/^[wh](\d+)?$/i.test(variableName);
}
/**
* Check if variable is math/algorithm context
*/
isMathContext(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
const prevLine = lines[line - 2] || '';
const nextLine = lines[line] || '';
// Check for math variable patterns
const mathPatterns = [
// Coordinate pairs: x1, y1, x2, y2
/^[xyz][12]$/i,
// Delta notation: dx, dy, dt, dr
/^d[xyztr]$/i,
// Math constants: a, b, c in equations
/^[abc]$/i,
// Vector components: vx, vy, vz
/^v[xyz]$/i,
// Position/point notation: p1, p2
/^p\d+$/i
];
if (mathPatterns.some(pattern => pattern.test(variableName))) {
return true;
}
// Context-based detection
const contextLines = [prevLine, currentLine, nextLine].join(' ');
// Check for math function names in the context
if (/function\s+(distance|calculate|compute|solve|formula|algorithm|equation|math)/i.test(contextLines)) {
return true;
}
// Math operations context
if (/[+\-*/=]\s*\w+|\w+\s*[+\-*/=]/.test(currentLine)) {
return true;
}
// Math functions context
if (/Math\.|sqrt|pow|abs|sin|cos|tan|distance|calculate/i.test(contextLines)) {
return true;
}
// Multiple single-char variables (typical in math)
if (/const\s+[a-z]\s*=.*[a-z]\s*=/.test(currentLine)) {
return true;
}
// Check for function parameters that are clearly coordinates/math
// Example: function distance(x1: number, y1: number, x2: number, y2: number)
if (/function.*\([^)]*\b(x1|y1|x2|y2|dx|dy|dz|dt)\b.*\)/.test(contextLines)) {
return true;
}
return false;
}
/**
* Check if variable is in callback/iteration context
*/
isCallbackContext(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
// Array methods with callback
return /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(currentLine);
}
/**
* Check if variable is in comparison/sorting context
*/
isComparisonContext(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
const prevLine = lines[line - 2] || '';
const nextLine = lines[line] || '';
const contextLines = [prevLine, currentLine, nextLine].join(' ');
// Comparison/sorting patterns
return /\.sort\s*\(/.test(contextLines) ||
/compare\s*\(/.test(contextLines) ||
/\w+\s*[<>=]+\s*\w+/.test(contextLines);
}
/**
* Check if variable is in event context
*/
isEventContext(content, variableName, line) {
const lines = content.split('\n');
const contextLines = lines.slice(Math.max(0, line - 2), line + 1).join(' ');
return /(?:event|evt|e)\s*[:=]|addEventListener|onClick|onSubmit|handler/i.test(contextLines);
}
/**
* Check if variable is in destructuring context
*/
isDestructuring(content, variableName, line) {
const lines = content.split('\n');
const currentLine = lines[line - 1] || '';
// Check for object destructuring
if (/const\s*{[^}]*}\s*=/.test(currentLine) || /{\s*\w+[^}]*}/.test(currentLine)) {
return true;
}
// Check for array destructuring
if (/const\s*\[[^\]]*\]\s*=/.test(currentLine) || /\[\s*\w+[^\]]*\]/.test(currentLine)) {
return true;
}
return false;
}
/**
* Analyze variable names in files
* @param {string[]} files - Array of file paths to analyze
* @param {string} language - Programming language
* @param {Object} options - Analysis options
* @returns {Object[]} Array of violations
*/
async analyze(files, language, options) {
const violations = [];
for (const filePath of files) {
try {
// Read file content
const fs = require('fs');
const content = fs.readFileSync(filePath, 'utf8');
// Analyze file content
const fileViolations = this.analyzeContent(content, filePath);
violations.push(...fileViolations);
} catch (error) {
console.error(`Error analyzing file ${filePath}:`, error.message);
}
}
return violations;
}
/**
* Clean line by removing comments but preserving structure for regex matching
* @param {string} line - Original line
* @returns {object} { cleanLine, commentRanges }
*/
cleanLineForMatching(line) {
return CommentDetector.cleanLineForMatching(line);
}
/**
* Check if a variable at a specific position is inside a comment
* @param {string} line - Line content
* @param {number} position - Position in line where variable was found
* @returns {boolean} True if position is inside a comment
*/
isPositionInComment(line, position) {
return CommentDetector.isPositionInComment(line, position);
}
/**
* Check if a line is a comment or inside a comment block
* @param {string} line - Line to check
* @param {number} lineIndex - Current line index
* @param {string[]} allLines - All lines in content
* @returns {boolean} True if line is commented
*/
isCommentedLine(line, lineIndex, allLines) {
const trimmedLine = line.trim();
// Check single-line comments
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('#')) {
return true;
}
// Check if we're inside a multi-line comment
let inComment = false;
for (let i = 0; i <= lineIndex; i++) {
const currentLine = allLines[i];
// Find /* and */ on the same line or across lines
let startPos = 0;
while (startPos < currentLine.length) {
const commentStart = currentLine.indexOf('/*', startPos);
const commentEnd = currentLine.indexOf('*/', startPos);
if (commentStart !== -1 && (commentEnd === -1 || commentStart < commentEnd)) {
inComment = true;
startPos = commentStart + 2;
} else if (commentEnd !== -1 && inComment) {
inComment = false;
startPos = commentEnd + 2;
} else {
break;
}
}
// If we're at the target line and inside a comment
if (i === lineIndex && inComment) {
return true;
}
}
return false;
}
/**
* Remove comments from content
* @param {string} content - File content
* @returns {string} Content with comments removed
*/
removeComments(content) {
let result = content;
// Remove single line comments (// comments)
result = result.replace(/\/\/.*$/gm, '');
// Remove multi-line comments (/* comments */)
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove # comments (for other languages like Python, Shell)
result = result.replace(/#.*$/gm, '');
return result;
}
/**
* Analyze variable names in content
*/
analyzeContent(content, filePath) {
// Ensure content is a string
if (typeof content !== 'string') {
console.error('Content is not a string:', typeof content);
return [];
}
const violations = [];
const lines = content.split('\n'); // Use original content to preserve line numbers
lines.forEach((line, index) => {
const lineNumber = index + 1;
// Skip commented lines
if (this.isCommentedLine(line, index, lines)) {
return;
}
// Skip empty lines
if (!line.trim()) {
return;
}
// Clean line for better pattern matching
const { cleanLine } = this.cleanLineForMatching(line);
// Match variable declarations on cleaned line
const patterns = [
// const/let/var declarations
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=:]/g,
// Function parameters in function declarations
/function\s+\w*\s*\(\s*([^)]+)\s*\)/g,
// Arrow function parameters
/\(\s*([^)]+)\s*\)\s*=>/g,
// Single parameter arrow functions
/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g,
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(cleanLine)) !== null) {
let variableNames = [];
if (match[1]) {
// Handle parameter lists
if (match[1].includes(',')) {
variableNames = match[1].split(',')
.map(param => param.trim().split(/[:\s=]/)[0].trim())
.filter(name => name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name));
} else {
const cleanName = match[1].trim().split(/[:\s=]/)[0].trim();
if (cleanName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(cleanName)) {
variableNames = [cleanName];
}
}
}
variableNames.forEach(variableName => {
// No need to check position since we're using cleaned line
const violation = this.checkVariableName(variableName, content, lineNumber, match.index);
if (violation) {
violations.push({
ruleId: 'C003',
severity: 'warning',
message: violation.message,
line: lineNumber,
column: match.index + 1,
filePath: filePath
});
}
});
}
});
});
return violations;
}
/**
* Check individual variable name
*/
checkVariableName(variableName, content, line, column) {
// Skip if empty or invalid
if (!variableName || typeof variableName !== 'string') {
return null;
}
// Skip TypeScript/React specific names
if (variableName.startsWith('_') || variableName.includes('$')) {
return null;
}
// Skip common React patterns
if (/^(useState|useEffect|useCallback|useMemo|useRef|useContext)$/.test(variableName)) {
return null;
}
// Check single character variables
if (variableName.length === 1) {
const lowerName = variableName.toLowerCase();
// Allow if in allowed single char list
if (this.options.allowedSingleChar.has(lowerName)) {
return null;
}
// Allow if in loop context
if (this.isLoopContext(content, variableName, line)) {
return null;
}
// Allow coordinates
if (this.isCoordinate(variableName)) {
return null;
}
// Allow in event context
if (lowerName === 'e' && this.isEventContext(content, variableName, line)) {
return null;
}
// Allow in math context (a, b, c coefficients)
if (['a', 'b', 'c'].includes(lowerName) && this.isMathContext(content, variableName, line)) {
return null;
}
// Allow a, b in comparison/sorting context
if (['a', 'b'].includes(lowerName) && this.isComparisonContext(content, variableName, line)) {
return null;
}
return {
message: `Variable '${variableName}' is only 1 character long. Use descriptive names (except for counters like i, j, k).`
};
}
const lowerName = variableName.toLowerCase();
// Check if it's an allowed abbreviation
if (this.options.allowedAbbreviations.has(lowerName)) {
return null;
}
// Allow in destructuring context
if (this.isDestructuring(content, variableName, line)) {
return null;
}
// Allow in callback/function parameter context for common terms
if (['item', 'data', 'element'].includes(lowerName) &&
(this.isCallbackContext(content, variableName, line) ||
this.isFunctionParameter(content, variableName, line))) {
return null;
}
// Check for minimum length
if (variableName.length < this.options.minLength) {
return {
message: `Variable '${variableName}' is too short (${variableName.length} characters). Use descriptive names with at least ${this.options.minLength} characters.`
};
}
// Check for math context BEFORE suspicious patterns
if (this.isMathContext(content, variableName, line)) {
return null;
}
// Check for suspicious patterns
for (const pattern of this.suspiciousPatterns) {
if (pattern.test(lowerName)) {
return {
message: `Variable '${variableName}' appears to be an unclear abbreviation. Use full descriptive names.`
};
}
}
// Check for unclear names
if (this.unclearNames.has(lowerName)) {
return {
message: `Variable '${variableName}' is unclear or ambiguous. Use more specific descriptive names.`
};
}
return null;
}
}
module.exports = C003NoVagueAbbreviations;