#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ JavaScript/TypeScript 代码扫描器 使用 ESLint 进行代码质量检查 """ import os import json import logging from typing import Dict, Any, List, Optional from scanner.base import BaseScanner logger = logging.getLogger(__name__) class JavaScriptScanner(BaseScanner): """JavaScript/TypeScript 代码扫描器""" def __init__(self, config: Dict[str, Any]): super().__init__(config) self.extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'] def scan(self, repo_url: str, commit_id: Optional[str], branch: str) -> Dict[str, Any]: """ 执行 JavaScript/TypeScript 代码扫描 Args: repo_url: 仓库 URL commit_id: 提交 ID branch: 分支名 Returns: 扫描结果 """ result = { 'tool': 'JavaScript Scanner', 'language': 'javascript', 'status': 'success', 'issues': [], 'summary': { 'total': 0, 'error': 0, 'warning': 0, 'info': 0 }, 'files_scanned': 0 } clone_dir = None try: # 克隆仓库 clone_dir = self.clone_repo(repo_url, commit_id, branch) # 获取 JavaScript/TypeScript 文件 js_files = self.get_changed_files(clone_dir, self.extensions) result['files_scanned'] = len(js_files) if not js_files: logger.info('没有找到 JavaScript/TypeScript 文件') return result # 运行 ESLint 扫描 eslint_result = self._run_eslint(clone_dir, js_files) # 合并结果 result['issues'] = eslint_result.get('issues', [])[:self.max_issues] if self.detailed else eslint_result.get('issues', []) result['summary'] = self._calculate_summary(eslint_result.get('issues', [])) result['raw_output'] = eslint_result.get('raw_output', '') except Exception as e: logger.error(f'JavaScript 扫描失败: {str(e)}') result['status'] = 'error' result['error'] = str(e) finally: # 清理临时目录 if clone_dir: self.cleanup(clone_dir) return result def _run_eslint(self, cwd: str, files: List[str]) -> Dict[str, Any]: """运行 ESLint 扫描""" result = { 'tool': 'eslint', 'issues': [], 'raw_output': '' } try: # 尝试使用 npx 运行 eslint cmd = ['npx', 'eslint', '--format=json', '--no-eslintrc'] + files # 如果没有 eslint 配置,先创建默认配置 eslintrc_path = os.path.join(cwd, '.eslintrc.json') if not os.path.exists(eslintrc_path): # 创建简单的 ESLint 配置 eslint_config = { "env": { "browser": True, "es2021": True, "node": True }, "extends": ["eslint:recommended"], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" } } with open(eslintrc_path, 'w') as f: json.dump(eslint_config, f) output = self.run_command(cmd, cwd, timeout=120) result['raw_output'] = output.get('stdout', '') + output.get('stderr', '') # 解析 JSON 输出 if output.get('stdout'): try: eslint_results = json.loads(output['stdout']) for file_result in eslint_results: file_path = file_result.get('filePath', '') messages = file_result.get('messages', []) for msg in messages: severity = 'error' if msg.get('severity', 0) == 2 else 'warning' result['issues'].append({ 'tool': 'eslint', 'type': severity, 'severity': 'Error' if msg.get('severity', 0) == 2 else 'Warning', 'message': msg.get('message', ''), 'file': os.path.basename(file_path), 'line': msg.get('line', 0), 'column': msg.get('column', 0), 'symbol': msg.get('ruleId', 'unknown') }) except json.JSONDecodeError as e: logger.warning(f'ESLint JSON 解析失败: {e}') except Exception as e: logger.warning(f'ESLint 运行失败: {str(e)}') return result def _calculate_summary(self, issues: List[Dict]) -> Dict[str, int]: """计算问题摘要""" summary = { 'total': len(issues), 'error': 0, 'warning': 0, 'info': 0 } for issue in issues: severity = issue.get('severity', '').lower() if severity in ['error', 'critical']: summary['error'] += 1 elif severity in ['warning', 'moderate']: summary['warning'] += 1 else: summary['info'] += 1 return summary