|
13 | 13 | from codesage.cli.plugin_loader import PluginManager |
14 | 14 | from codesage.history.store import StorageEngine |
15 | 15 | from codesage.core.interfaces import CodeIssue |
| 16 | +from codesage.risk.risk_scorer import RiskScorer |
| 17 | +from codesage.config.risk_baseline import RiskBaselineConfig |
| 18 | +from codesage.rules.jules_specific_rules import JULES_RULESET |
| 19 | +from codesage.rules.base import RuleContext |
16 | 20 | from datetime import datetime, timezone |
17 | 21 |
|
18 | 22 | def get_builder(language: str, path: Path): |
@@ -144,8 +148,10 @@ def merge_snapshots(snapshots: List[ProjectSnapshot], project_name: str) -> Proj |
144 | 148 | @click.option('--ci-mode', is_flag=True, help='Enable CI mode (auto-detect GitHub environment).') |
145 | 149 | @click.option('--plugins-dir', default='.codesage/plugins', help='Directory containing plugins.') |
146 | 150 | @click.option('--db-url', default='sqlite:///codesage.db', help='Database URL for storage.') |
| 151 | +@click.option('--git-repo', type=click.Path(), help='Git 仓库路径(用于变更历史分析)') |
| 152 | +@click.option('--coverage-report', type=click.Path(), help='覆盖率报告路径(Cobertura/JaCoCo XML)') |
147 | 153 | @click.pass_context |
148 | | -def scan(ctx, path, language, reporter, output, fail_on_high, ci_mode, plugins_dir, db_url): |
| 154 | +def scan(ctx, path, language, reporter, output, fail_on_high, ci_mode, plugins_dir, db_url, git_repo, coverage_report): |
149 | 155 | """ |
150 | 156 | Scan the codebase and report issues. |
151 | 157 | """ |
@@ -205,16 +211,73 @@ def scan(ctx, path, language, reporter, output, fail_on_high, ci_mode, plugins_d |
205 | 211 | click.echo(f"Failed to merge snapshots: {e}", err=True) |
206 | 212 | ctx.exit(1) |
207 | 213 |
|
208 | | - # 3. Apply Custom Rules (Plugins) |
| 214 | + # Populate file contents if missing (needed for rules) |
| 215 | + click.echo("Populating file contents...") |
| 216 | + for file_snapshot in snapshot.files: |
| 217 | + if not file_snapshot.content: |
| 218 | + try: |
| 219 | + full_path = root_path / file_snapshot.path |
| 220 | + if full_path.exists(): |
| 221 | + file_snapshot.content = full_path.read_text(errors='ignore') |
| 222 | + # Update size if missing |
| 223 | + if file_snapshot.size is None: |
| 224 | + file_snapshot.size = len(file_snapshot.content) |
| 225 | + except Exception as e: |
| 226 | + # logger.warning(f"Failed to read file {file_snapshot.path}: {e}") |
| 227 | + pass |
| 228 | + |
| 229 | + # 3. Apply Risk Scoring (Enhanced in Phase 1) |
| 230 | + try: |
| 231 | + risk_config = RiskBaselineConfig() # Load default config |
| 232 | + scorer = RiskScorer( |
| 233 | + config=risk_config, |
| 234 | + repo_path=git_repo or path, # Default to scanned path if not specified |
| 235 | + coverage_report=coverage_report |
| 236 | + ) |
| 237 | + snapshot = scorer.score_project(snapshot) |
| 238 | + except Exception as e: |
| 239 | + click.echo(f"Warning: Risk scoring failed: {e}", err=True) |
| 240 | + |
| 241 | + # 4. Apply Custom Rules (Plugins & Jules Rules) |
| 242 | + |
| 243 | + # Create RuleContext |
| 244 | + # We need a dummy config for now as RuleContext expects one, but JulesRules might not use it. |
| 245 | + # However, PythonRulesetBaselineConfig is expected by RuleContext definition in base.py. |
| 246 | + # We need to import it or mock it. |
| 247 | + from codesage.config.rules_python_baseline import RulesPythonBaselineConfig |
| 248 | + rule_config = RulesPythonBaselineConfig() # Default config |
| 249 | + |
| 250 | + # Apply Jules Specific Rules |
| 251 | + click.echo("Applying Jules-specific rules...") |
| 252 | + for rule in JULES_RULESET: |
| 253 | + for file_snapshot in snapshot.files: |
| 254 | + try: |
| 255 | + # Create context for this file |
| 256 | + rule_ctx = RuleContext( |
| 257 | + project=snapshot, |
| 258 | + file=file_snapshot, |
| 259 | + config=rule_config |
| 260 | + ) |
| 261 | + |
| 262 | + # Call rule.check(ctx) |
| 263 | + # Ensure rule supports check(ctx) |
| 264 | + issues = rule.check(rule_ctx) |
| 265 | + |
| 266 | + if issues: |
| 267 | + if file_snapshot.issues is None: |
| 268 | + file_snapshot.issues = [] |
| 269 | + file_snapshot.issues.extend(issues) |
| 270 | + except Exception as e: |
| 271 | + click.echo(f"Error applying rule {rule.rule_id} to {file_snapshot.path}: {e}", err=True) |
| 272 | + |
| 273 | + # Apply Plugin Rules |
209 | 274 | for rule in plugin_manager.rules: |
210 | 275 | # Ensure we iterate over the list of files |
211 | 276 | for file_snapshot in snapshot.files: |
212 | 277 | file_path = Path(file_snapshot.path) |
213 | 278 | try: |
214 | | - content = "" |
215 | | - full_path = root_path / file_path |
216 | | - if full_path.exists(): |
217 | | - content = full_path.read_text(errors='ignore') |
| 279 | + # Content is already populated now |
| 280 | + content = file_snapshot.content or "" |
218 | 281 |
|
219 | 282 | issues = rule.check(str(file_path), content, {}) |
220 | 283 | if issues: |
@@ -249,29 +312,33 @@ def scan(ctx, path, language, reporter, output, fail_on_high, ci_mode, plugins_d |
249 | 312 | except Exception as e: |
250 | 313 | click.echo(f"Error running rule {rule.id} on {file_path}: {e}", err=True) |
251 | 314 |
|
252 | | - # Recalculate Issues Summary after Plugins |
253 | | - # Simplified recalculation |
| 315 | + # Recalculate Issues Summary after Plugins & Jules Rules |
254 | 316 | total_issues = 0 |
255 | 317 | by_severity = {} |
| 318 | + by_rule = {} |
256 | 319 |
|
257 | 320 | for f in snapshot.files: |
258 | 321 | if f.issues: |
259 | 322 | total_issues += len(f.issues) |
260 | 323 | for issue in f.issues: |
261 | 324 | by_severity[issue.severity] = by_severity.get(issue.severity, 0) + 1 |
| 325 | + if issue.rule_id: |
| 326 | + by_rule[issue.rule_id] = by_rule.get(issue.rule_id, 0) + 1 |
262 | 327 |
|
263 | 328 | # Update snapshot summary if issues changed |
264 | 329 | if snapshot.issues_summary: |
265 | 330 | snapshot.issues_summary.total_issues = total_issues |
266 | 331 | snapshot.issues_summary.by_severity = by_severity |
| 332 | + snapshot.issues_summary.by_rule = by_rule |
267 | 333 | else: |
268 | 334 | snapshot.issues_summary = ProjectIssuesSummary( |
269 | 335 | total_issues=total_issues, |
270 | | - by_severity=by_severity |
| 336 | + by_severity=by_severity, |
| 337 | + by_rule=by_rule |
271 | 338 | ) |
272 | 339 |
|
273 | 340 |
|
274 | | - # 4. Save to Storage |
| 341 | + # 5. Save to Storage |
275 | 342 | if storage: |
276 | 343 | try: |
277 | 344 | storage.save_snapshot(snapshot.metadata.project_name, snapshot) |
|
0 commit comments