162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
#!/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, changed_files: Optional[List[str]] = None) -> Dict[str, Any]:
|
||
"""
|
||
执行 JavaScript/TypeScript 代码扫描
|
||
|
||
Args:
|
||
repo_url: 仓库 URL
|
||
commit_id: 提交 ID
|
||
branch: 分支名
|
||
changed_files: 可选的变更文件列表(来自 PR)
|
||
|
||
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, changed_files)
|
||
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)
|
||
|
||
|
||
return result
|
||
|
||
def _run_eslint(self, clone_dir: 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(clone_dir, '.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, clone_dir, 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', '')
|
||
# 使用相对于 clone_dir 的路径
|
||
rel_path = os.path.relpath(file_path, clone_dir) if file_path else ''
|
||
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': rel_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
|