agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
172 lines (145 loc) • 5.04 kB
JavaScript
/**
* @file AST-based repeated DOM query detector
* @description Detects repeated DOM queries using AST analysis
*/
const { parseToAST, getNodeLine, getNodeSource, walk } = require('./astParser');
const { PERFORMANCE_PATTERNS } = require('../performancePatterns');
/**
* Detect repeated DOM queries using AST
* @param {string} content - File content
* @param {string} filePath - File path
* @returns {Array} Array of repeated DOM query issues
*/
function detectRepeatedDomQueriesAST(content, filePath) {
const issues = [];
const ast = parseToAST(content, filePath);
if (!ast) return [];
const pattern = PERFORMANCE_PATTERNS['repeated_dom_query'];
const queryMap = new Map();
// Find all DOM query calls
walk.simple(ast, {
CallExpression(node) {
const queryInfo = extractDomQuery(node);
if (queryInfo) {
const key = `${queryInfo.method}:${queryInfo.selector}`;
if (!queryMap.has(key)) {
queryMap.set(key, []);
}
queryMap.get(key).push({
node,
line: getNodeLine(node),
code: getNodeSource(content, node).trim(),
method: queryInfo.method,
selector: queryInfo.selector
});
}
}
});
// Find repeated queries in same scope
queryMap.forEach((occurrences, key) => {
if (occurrences.length > 1) {
// Group by function scope
const scopeGroups = groupByScope(occurrences, ast);
scopeGroups.forEach(group => {
if (group.length > 1) {
const [method, selector] = key.split(':');
issues.push({
type: 'repeated_dom_query',
severity: pattern.severity,
category: pattern.category,
location: `${filePath}:${group[0].line},${group[group.length - 1].line}`,
line: group[0].line,
selector: selector,
queryType: method,
queryCount: group.length,
code: group[0].code,
description: `DOM query for "${selector}" repeated ${group.length} times in same scope`,
summary: `Repeated DOM query for "${selector}"`,
recommendation: 'Cache DOM query result in a variable to avoid repeated lookups',
effort: pattern.effort,
impact: pattern.impact,
estimatedSavings: '30-60% DOM performance improvement'
});
}
});
}
});
return issues;
}
function extractDomQuery(node) {
// document.getElementById('...')
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'document') {
const method = node.callee.property.name;
const domMethods = ['getElementById', 'getElementsByClassName', 'getElementsByTagName',
'querySelector', 'querySelectorAll'];
if (domMethods.includes(method) && node.arguments[0]) {
const arg = node.arguments[0];
if (arg.type === 'Literal' && typeof arg.value === 'string') {
return {
method,
selector: arg.value
};
}
}
}
// jQuery: $('...') or jQuery('...')
if ((node.callee.name === '$' || node.callee.name === 'jQuery') &&
node.arguments[0] &&
node.arguments[0].type === 'Literal' &&
typeof node.arguments[0].value === 'string') {
return {
method: 'jQuery',
selector: node.arguments[0].value
};
}
return null;
}
function groupByScope(occurrences, ast) {
const groups = [];
const processed = new Set();
occurrences.forEach(occurrence => {
if (processed.has(occurrence)) return;
const group = [occurrence];
processed.add(occurrence);
// Find other occurrences in same function scope
const scope = findFunctionScope(occurrence.node, ast);
occurrences.forEach(other => {
if (other !== occurrence && !processed.has(other)) {
const otherScope = findFunctionScope(other.node, ast);
// Same scope and within reasonable distance
if (scope === otherScope && Math.abs(other.line - occurrence.line) <= 50) {
group.push(other);
processed.add(other);
}
}
});
if (group.length > 1) {
groups.push(group.sort((a, b) => a.line - b.line));
}
});
return groups;
}
function findFunctionScope(node, ast) {
let functionId = null;
walk.ancestor(ast, {
[node.type]: (currentNode, ancestors) => {
if (currentNode === node) {
// Find the nearest function ancestor
for (let i = ancestors.length - 1; i >= 0; i--) {
const ancestor = ancestors[i];
if (ancestor.type === 'FunctionDeclaration' ||
ancestor.type === 'FunctionExpression' ||
ancestor.type === 'ArrowFunctionExpression') {
functionId = ancestor.start; // Use start position as unique ID
break;
}
}
}
}
});
return functionId;
}
module.exports = {
detectRepeatedDomQueriesAST
};