code-auditor-mcp
Version:
TypeScript/JavaScript code quality auditor with MCP server - Analyze code for SOLID principles, DRY violations, security patterns, and more
423 lines • 18 kB
JavaScript
/**
* QueryParser - Parses search queries into structured format with tokenization and synonym expansion
*
* Supports:
* - Multi-word queries with intelligent tokenization
* - Exact phrase matching with quotes
* - Excluded terms with minus operator
* - Special operators (type:, param:, return:, etc.)
* - Synonym expansion for common programming terms
* - Fuzzy search and stemming flags
*/
export class QueryParser {
// Comprehensive synonym mappings for programming terms
static SYNONYMS = {
// Common programming verbs
'get': ['fetch', 'retrieve', 'obtain', 'find', 'query', 'load', 'read'],
'set': ['update', 'save', 'store', 'write', 'assign', 'modify', 'change'],
'create': ['make', 'new', 'generate', 'build', 'construct', 'initialize', 'init'],
'delete': ['remove', 'destroy', 'drop', 'clear', 'purge', 'erase'],
'add': ['append', 'insert', 'push', 'attach', 'include'],
'remove': ['delete', 'pop', 'detach', 'exclude', 'eliminate'],
// Data structures
'array': ['list', 'vector', 'collection', 'arr'],
'object': ['obj', 'dict', 'dictionary', 'map', 'hash', 'record'],
'string': ['str', 'text', 'chars', 'characters'],
'number': ['num', 'int', 'integer', 'float', 'double', 'numeric'],
'boolean': ['bool', 'flag', 'true/false'],
// Common patterns
'validate': ['check', 'verify', 'test', 'ensure', 'confirm'],
'handler': ['listener', 'callback', 'processor', 'controller'],
'util': ['utility', 'helper', 'tools', 'utils'],
'config': ['configuration', 'settings', 'options', 'conf'],
'auth': ['authentication', 'authorization', 'authenticate', 'authorize'],
'user': ['users', 'person', 'account', 'member', 'client'],
'error': ['err', 'exception', 'fault', 'failure', 'problem'],
'log': ['logger', 'logging', 'trace', 'debug', 'console'],
// HTTP/API related
'api': ['endpoint', 'route', 'service', 'rest', 'graphql'],
'request': ['req', 'http', 'call', 'fetch'],
'response': ['res', 'reply', 'result', 'output'],
// Database related
'database': ['db', 'storage', 'datastore', 'repository'],
'query': ['sql', 'search', 'find', 'select'],
'table': ['collection', 'entity', 'model', 'schema'],
// React/Frontend specific
'component': ['comp', 'widget', 'element', 'view', 'react', 'ui'],
'render': ['display', 'show', 'present', 'draw', 'renders', 'paint'],
'state': ['status', 'data', 'store', 'context'],
'props': ['properties', 'attributes', 'params', 'arguments'],
// Async patterns
'async': ['asynchronous', 'promise', 'await', 'concurrent'],
'sync': ['synchronous', 'blocking', 'sequential'],
'callback': ['cb', 'handler', 'listener', 'oncomplete'],
// Testing
'test': ['tests', 'spec', 'suite', 'unit', 'integration', 'e2e'],
'mock': ['stub', 'fake', 'spy', 'double'],
// Common abbreviations
'fn': ['function', 'func', 'method'],
'param': ['parameter', 'arg', 'argument'],
'return': ['returns', 'output', 'result'],
'doc': ['documentation', 'docs', 'comment', 'jsdoc'],
// React-specific (additional)
'hook': ['hooks', 'useeffect', 'usestate', 'usememo', 'usecallback'],
'jsx': ['tsx', 'react-element', 'markup'],
'lifecycle': ['mount', 'unmount', 'update', 'effect']
};
// Special operators that can be used in queries
static OPERATORS = {
'type:': 'fileType',
'file:': 'filePath',
'path:': 'filePath',
'lang:': 'language',
'language:': 'language',
'param:': 'parameters',
'parameter:': 'parameters',
'return:': 'returnType',
'returns:': 'returnType',
'complexity:': 'complexity',
'jsdoc:': 'hasJsDoc',
'doc:': 'hasJsDoc',
'since:': 'dateRange',
'before:': 'dateRange',
'after:': 'dateRange',
// React-specific operators
'component:': 'componentType',
'hook:': 'hasHook',
'hooks:': 'hasHook',
'prop:': 'hasProp',
'props:': 'hasProp',
'entity:': 'entityType',
// Dependency operators
'dep:': 'usesDependency',
'dependency:': 'usesDependency',
'uses:': 'usesDependency',
'calls:': 'callsFunction',
'calledby:': 'calledByFunction',
'dependents-of:': 'calledByFunction',
'used-by:': 'calledByFunction',
'depends-on:': 'dependsOnModule',
'imports-from:': 'dependsOnModule',
'unused-imports': 'hasUnusedImports',
'dead-imports': 'hasUnusedImports'
};
/**
* Parse a search query string into a structured ParsedQuery object
* @param query The raw search query string
* @returns ParsedQuery object with extracted terms, filters, and options
*/
parse(query) {
const result = {
terms: [],
originalTerms: [],
phrases: [],
excludedTerms: [],
filters: {},
searchFields: undefined,
fuzzy: false,
stemming: false
};
if (!query || query.trim().length === 0) {
return result;
}
// Extract exact phrases (quoted strings) first
const phrases = this.extractPhrases(query);
result.phrases = phrases.map(p => p.value);
// Remove phrases from query for further processing
let remainingQuery = query;
phrases.forEach(phrase => {
remainingQuery = remainingQuery.replace(phrase.original, ' ');
});
// Extract operators and their values
const { operators, cleanedQuery } = this.extractOperators(remainingQuery);
this.applyOperatorFilters(operators, result);
// Process remaining tokens
const tokens = this.tokenize(cleanedQuery);
for (const token of tokens) {
if (token.startsWith('-')) {
// Excluded term
const term = token.slice(1);
if (term.length > 0) {
result.excludedTerms.push(term);
// Also add synonyms to excluded terms
const synonyms = this.getSynonyms(term);
result.excludedTerms.push(...synonyms);
}
}
else if (token === '~' || token === 'fuzzy') {
// Enable fuzzy search
result.fuzzy = true;
}
else if (token === 'stem' || token === 'stemming') {
// Enable stemming
result.stemming = true;
}
else if (token === 'unused-imports' || token === 'dead-imports') {
// Special operators without colons
if (!result.filters.metadata) {
result.filters.metadata = {};
}
result.filters.metadata.hasUnusedImports = true;
}
else {
// Regular search term
result.originalTerms.push(token);
result.terms.push(token);
// Add synonyms for the term
const synonyms = this.getSynonyms(token);
result.terms.push(...synonyms);
}
}
// Remove duplicates
result.originalTerms = [...new Set(result.originalTerms)];
result.terms = [...new Set(result.terms)];
result.excludedTerms = [...new Set(result.excludedTerms)];
result.phrases = [...new Set(result.phrases)];
// Set default search fields if not specified
if (!result.searchFields || result.searchFields.length === 0) {
result.searchFields = ['name', 'signature', 'jsDoc', 'purpose', 'context'];
}
return result;
}
/**
* Extract quoted phrases from the query
* @param query The query string
* @returns Array of phrase objects with original and cleaned value
*/
extractPhrases(query) {
const phrases = [];
const phraseRegex = /["']([^"']+)["']/g;
let match;
while ((match = phraseRegex.exec(query)) !== null) {
phrases.push({
original: match[0],
value: match[1].trim()
});
}
return phrases;
}
/**
* Extract operators and their values from the query
* @param query The query string
* @returns Object with operators map and cleaned query
*/
extractOperators(query) {
const operators = new Map();
let cleanedQuery = query;
// Match operator:value patterns
const operatorRegex = /(\w+):(\S+)/g;
let match;
while ((match = operatorRegex.exec(query)) !== null) {
const operator = match[1].toLowerCase() + ':';
const value = match[2];
if (operator in QueryParser.OPERATORS) {
operators.set(operator, value);
cleanedQuery = cleanedQuery.replace(match[0], ' ');
}
}
return { operators, cleanedQuery: cleanedQuery.trim() };
}
/**
* Apply operator filters to the parsed query
* @param operators Map of operators and values
* @param parsedQuery The parsed query object to update
*/
applyOperatorFilters(operators, parsedQuery) {
operators.forEach((value, operator) => {
const filterKey = QueryParser.OPERATORS[operator];
switch (filterKey) {
case 'fileType':
parsedQuery.filters.fileType = value;
break;
case 'filePath':
parsedQuery.filters.filePath = value;
break;
case 'language':
parsedQuery.filters.language = value;
break;
case 'hasJsDoc':
parsedQuery.filters.hasJsDoc = value.toLowerCase() === 'true' || value === '1';
break;
case 'complexity':
if (value.includes('-')) {
const [min, max] = value.split('-').map(v => parseInt(v.trim(), 10));
if (!isNaN(min) && !isNaN(max)) {
parsedQuery.filters.complexity = { min, max };
}
}
else {
const complexityValue = parseInt(value, 10);
if (!isNaN(complexityValue)) {
parsedQuery.filters.complexity = {
min: complexityValue,
max: complexityValue
};
}
}
break;
case 'dateRange':
if (!parsedQuery.filters.dateRange) {
parsedQuery.filters.dateRange = {};
}
const date = new Date(value);
if (!isNaN(date.getTime())) {
if (operator === 'since:' || operator === 'after:') {
parsedQuery.filters.dateRange.start = date;
}
else if (operator === 'before:') {
parsedQuery.filters.dateRange.end = date;
}
}
break;
case 'parameters':
// Add parameter search to search fields
if (!parsedQuery.searchFields) {
parsedQuery.searchFields = [];
}
parsedQuery.searchFields.push('parameters');
parsedQuery.terms.push(value);
break;
case 'returnType':
// Add return type to search fields
if (!parsedQuery.searchFields) {
parsedQuery.searchFields = [];
}
parsedQuery.searchFields.push('returnType');
parsedQuery.terms.push(value);
break;
case 'componentType':
// Filter by React component type
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.componentType = value;
break;
case 'hasHook':
// Search for components using specific hooks
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.hasHook = value;
break;
case 'hasProp':
// Search for components with specific props
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.hasProp = value;
break;
case 'entityType':
// Filter by entity type (function vs component)
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.entityType = value;
break;
case 'usesDependency':
// Filter by external dependency usage
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.usesDependency = value;
break;
case 'callsFunction':
// Filter by functions that this function calls
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.callsFunction = value;
break;
case 'calledByFunction':
// Filter by functions that call this function
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.calledByFunction = value;
break;
case 'dependsOnModule':
// Filter by module/file dependencies
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.dependsOnModule = value;
break;
case 'hasUnusedImports':
// Special filter for unused imports (boolean)
if (!parsedQuery.filters.metadata) {
parsedQuery.filters.metadata = {};
}
parsedQuery.filters.metadata.hasUnusedImports = true;
break;
}
});
}
/**
* Tokenize a string into individual words
* @param text The text to tokenize
* @returns Array of tokens
*/
tokenize(text) {
// Split on whitespace and common delimiters, but preserve hyphenated words
const tokens = text
.toLowerCase()
.split(/\s+/)
.filter(token => token.length > 0);
// Further split camelCase and snake_case
const expandedTokens = [];
for (const token of tokens) {
// Split camelCase
const camelCaseTokens = token.split(/(?=[A-Z])/).filter(t => t.length > 0);
if (camelCaseTokens.length > 1) {
expandedTokens.push(token); // Keep original
expandedTokens.push(...camelCaseTokens.map(t => t.toLowerCase()));
}
else {
// Split snake_case
const snakeCaseTokens = token.split('_').filter(t => t.length > 0);
if (snakeCaseTokens.length > 1) {
expandedTokens.push(token); // Keep original
expandedTokens.push(...snakeCaseTokens);
}
else {
expandedTokens.push(token);
}
}
}
return [...new Set(expandedTokens)];
}
/**
* Get synonyms for a given term
* @param term The term to find synonyms for
* @returns Array of synonyms (not including the original term)
*/
getSynonyms(term) {
const synonyms = [];
const lowerTerm = term.toLowerCase();
// Check if term is a direct key
if (QueryParser.SYNONYMS[lowerTerm]) {
synonyms.push(...QueryParser.SYNONYMS[lowerTerm]);
}
// Check if term is a value in any synonym group
for (const [key, values] of Object.entries(QueryParser.SYNONYMS)) {
if (values.includes(lowerTerm)) {
synonyms.push(key);
// Add other synonyms from the same group
synonyms.push(...values.filter(v => v !== lowerTerm));
}
}
return [...new Set(synonyms)];
}
/**
* Expand a query with synonyms (utility method)
* @param query The original query
* @returns Expanded query with synonyms
*/
expandQuery(query) {
const parsed = this.parse(query);
const allTerms = [...new Set([...parsed.terms, ...parsed.phrases])];
return allTerms.join(' ');
}
}
// Export a singleton instance for convenience
export const queryParser = new QueryParser();
//# sourceMappingURL=QueryParser.js.map