UNPKG

ai-linter

Version:

A linter tool to analyze JavaScript, TypeScript, and Vue code, with a focus on AI-generated code, unused imports, alias resolution, and potential hazards.

288 lines (275 loc) 10.2 kB
const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const globals = require('./globals'); const consoleMethods = new Set(['log', 'warn', 'error', 'info', 'debug', 'table', 'dir', 'assert', 'count', 'time', 'timeEnd', 'trace']); function isGlobalOrCommonBuiltIn(name) { return globals.has(name); } class AstDataExtractor { extractData(code, filePath, fileExtension) { try { const plugins = ['jsx', 'dynamicImport', 'decorators-legacy']; if (['.ts', '.tsx'].includes(fileExtension)) { plugins.push('typescript'); } let ast; if (fileExtension === '.vue') { const scriptMatch = code.match(/<script[^>]*>([\s\S]*?)<\/script>/); if (!scriptMatch) { return { syntaxError: null, imports: [], dynamicImports: [], exports: [], unusedImports: [], codeIssues: [], comments: [] }; } const scriptCode = scriptMatch[1]; ast = parser.parse(scriptCode, { sourceType: 'module', plugins: ['jsx', 'dynamicImport', 'typescript'], attachComment: true }); } else { ast = parser.parse(code, { sourceType: 'unambiguous', plugins, attachComment: true }); } return this._traverseAst(ast, filePath); } catch (error) { return { syntaxError: { message: error.message, line: error.loc?.line, column: error.loc?.column }, imports: [], dynamicImports: [], exports: [], unusedImports: [], codeIssues: [], comments: [] }; } } _traverseAst(ast, filePath) { const imports = []; const dynamicImports = []; const exports = []; const unusedImports = []; const importedIdentifiers = new Set(); const usedIdentifiers = new Set(); const codeIssues = []; const comments = []; traverse(ast, { ImportDeclaration({ node }) { const importPath = node.source.value; const specifiers = node.specifiers.map(spec => { let name, alias; if (spec.type === 'ImportDefaultSpecifier') { name = 'default'; alias = spec.local.name; } else if (spec.type === 'ImportNamespaceSpecifier') { name = '*'; alias = spec.local.name; } else { name = spec.imported.name; alias = spec.local.name; } importedIdentifiers.add(alias); return { name, alias, type: spec.type }; }); imports.push({ path: importPath, specifiers, line: node.loc?.start.line }); }, CallExpression({ node }) { if (node.callee.name === 'require' && node.arguments.length === 1 && node.arguments[0].type === 'StringLiteral') { imports.push({ path: node.arguments[0].value, specifiers: [{ name: 'default', alias: null, type: 'require' }], line: node.loc?.start.line, isDynamic: false }); } if (node.callee.type === 'Import' && node.arguments.length === 1 && node.arguments[0].type === 'StringLiteral') { dynamicImports.push({ path: node.arguments[0].value, line: node.loc?.start.line }); } if (node.callee.name === 'eval') { codeIssues.push({ type: 'eval-usage', message: "Usage of 'eval' is discouraged.", line: node.loc?.start.line }); } if (node.callee.type === 'MemberExpression' && node.callee.object.name === 'console' && node.callee.property.name && consoleMethods.has(node.callee.property.name)) { codeIssues.push({ type: 'console-usage', message: `Usage of 'console.${node.callee.property.name}'.`, line: node.loc?.start.line, identifierName: `console.${node.callee.property.name}` }); } }, ExportNamedDeclaration({ node }) { if (node.source) { exports.push({ type: 're-export', from: node.source.value, specifiers: node.specifiers.map(spec => ({ localName: spec.local.name, exportedName: spec.exported.name })), line: node.loc?.start.line }); } else if (node.declaration) { const declarationNames = []; if (node.declaration.type === 'VariableDeclaration') { node.declaration.declarations.forEach(declarator => { if (declarator.id.type === 'Identifier') { declarationNames.push(declarator.id.name); } }); } else if (node.declaration.id && node.declaration.id.name) { declarationNames.push(node.declaration.id.name); } if (declarationNames.length > 0) { exports.push({ type: 'named', specifiers: declarationNames, line: node.loc?.start.line }); } } else if (node.specifiers && node.specifiers.length > 0) { exports.push({ type: 'named', specifiers: node.specifiers.map(spec => spec.exported.name), line: node.loc?.start.line }); } }, ExportDefaultDeclaration({ node }) { exports.push({ type: 'default', line: node.loc?.start.line }); }, ExportAllDeclaration({ node }) { exports.push({ type: 'all', from: node.source.value, line: node.loc?.start.line }); }, Identifier(path) { const { node } = path; if (importedIdentifiers.has(node.name)) { usedIdentifiers.add(node.name); } if (path.isReferencedIdentifier()) { const parentType = path.parentPath.node.type; const isPropertyAccess = (parentType === 'MemberExpression' || parentType === 'OptionalMemberExpression') && path.key === 'property' && !path.parentPath.node.computed; const isObjectOrClassKey = (path.key === 'key' && !path.parentPath.node.computed && (parentType === 'ObjectProperty' || parentType === 'ClassProperty' || parentType === 'MethodDefinition')); const isLabel = parentType === 'LabeledStatement' || parentType === 'BreakStatement' || parentType === 'ContinueStatement'; const isImportedSpecifierName = parentType === 'ImportSpecifier' && path.key === 'imported'; if (isPropertyAccess || isObjectOrClassKey || isLabel || isImportedSpecifierName) { } else if (!path.scope.hasBinding(node.name) && !isGlobalOrCommonBuiltIn(node.name)) { codeIssues.push({ type: 'undeclared-identifier', message: `Identifier '${node.name}' is not declared.`, line: node.loc?.start.line, identifierName: node.name }); } } }, JSXOpeningElement(path) { const { node } = path; if (node.name.type === 'JSXIdentifier') { const componentName = node.name.name; if (componentName[0] === componentName[0].toUpperCase() && !(componentName[0] >= '0' && componentName[0] <= '9')) { if (!path.scope.hasBinding(componentName) && !importedIdentifiers.has(componentName) && !isGlobalOrCommonBuiltIn(componentName)) { codeIssues.push({ type: 'undeclared-jsx-component', message: `JSX component '<${componentName}>' is not declared or imported.`, line: node.loc?.start.line, identifierName: componentName }); } } } }, DebuggerStatement({ node }) { codeIssues.push({ type: 'debugger-statement', message: "'debugger' statement found.", line: node.loc?.start.line }); }, ObjectExpression(path) { const { node } = path; const seenKeys = new Set(); for (const prop of node.properties) { if (prop.type === 'ObjectProperty' && !prop.computed) { let keyName; if (prop.key.type === 'Identifier') { keyName = prop.key.name; } else if (prop.key.type === 'StringLiteral' || prop.key.type === 'NumericLiteral') { keyName = String(prop.key.value); } if (keyName !== undefined) { if (seenKeys.has(keyName)) { codeIssues.push({ type: 'duplicate-object-key', message: `Duplicate key '${keyName}' in object literal.`, line: prop.key.loc?.start.line, identifierName: keyName }); } else { seenKeys.add(keyName); } } } } }, enter(path) { if (path.node.leadingComments || path.node.trailingComments || path.node.innerComments) { const nodeComments = [ ...(path.node.leadingComments || []), ...(path.node.trailingComments || []), ...(path.node.innerComments || []) ]; nodeComments.forEach(comment => { const commentText = comment.value.trim().toUpperCase(); if (commentText.startsWith('TODO') || commentText.startsWith('FIXME')) { comments.push({ type: commentText.startsWith('TODO') ? 'todo' : 'fixme', text: comment.value.trim(), line: comment.loc.start.line, column: comment.loc.start.column }); } }); } } }); for (const identifier of importedIdentifiers) { if (!usedIdentifiers.has(identifier)) { unusedImports.push(identifier); } } return { syntaxError: null, imports, dynamicImports, exports, unusedImports, codeIssues, comments }; } } module.exports = AstDataExtractor;