diff --git a/semantic_code_intelligence/ci/hotspots.py b/semantic_code_intelligence/ci/hotspots.py index cd88e92..5c9ca1b 100644 --- a/semantic_code_intelligence/ci/hotspots.py +++ b/semantic_code_intelligence/ci/hotspots.py @@ -172,7 +172,16 @@ def analyze_hotspots( w_fan_out += extra w_churn = 0.0 - callable_symbols = [s for s in symbols if s.kind in ("function", "method")] + # Deduplicate by (file_path, name) so that re-indexed or multiply-parsed + # symbols don't appear as separate hotspot entries with identical scores. + _seen: set[tuple[str, str]] = set() + callable_symbols = [] + for s in symbols: + if s.kind in ("function", "method"): + key = (s.file_path, s.name) + if key not in _seen: + _seen.add(key) + callable_symbols.append(s) # ── Per-symbol raw metrics ─────────────────────────────────── # Complexity diff --git a/semantic_code_intelligence/ci/quality.py b/semantic_code_intelligence/ci/quality.py index eb61277..3803a20 100644 --- a/semantic_code_intelligence/ci/quality.py +++ b/semantic_code_intelligence/ci/quality.py @@ -354,8 +354,62 @@ def detect_duplicates( # ── Bandit security linting (optional) ─────────────────────────────── try: - from bandit.core import manager as _bandit_manager - from bandit.core import config as _bandit_config + import logging as _logging + + # Bandit eagerly loads all its formatters at import time, including a + # SARIF formatter that requires the optional `sarif_om` package. When + # `sarif_om` is absent bandit logs an ERROR that appears on every run + # even when SARIF output was never requested. Suppress that specific + # message during the import so users only see errors that are relevant + # to them. + class _SuppressSarifFilter(_logging.Filter): + def filter(self, record: _logging.LogRecord) -> bool: + return "Could not load 'sarif'" not in record.getMessage() + + def _logger_ancestry(logger: _logging.Logger) -> list[_logging.Logger]: + ancestry: list[_logging.Logger] = [] + current: _logging.Logger | None = logger + while current is not None: + ancestry.append(current) + if not current.propagate: + break + parent = current.parent + current = parent if isinstance(parent, _logging.Logger) else None + return ancestry + + def _add_filter_to_handlers( + logger: _logging.Logger, + log_filter: _logging.Filter, + ) -> list[_logging.Handler]: + filtered_handlers: list[_logging.Handler] = [] + seen_handlers: set[int] = set() + for current_logger in _logger_ancestry(logger): + for handler in current_logger.handlers: + handler_id = id(handler) + if handler_id in seen_handlers: + continue + handler.addFilter(log_filter) + filtered_handlers.append(handler) + seen_handlers.add(handler_id) + return filtered_handlers + + def _remove_filter_from_handlers( + handlers: list[_logging.Handler], + log_filter: _logging.Filter, + ) -> None: + for handler in handlers: + handler.removeFilter(log_filter) + + _bandit_root = _logging.getLogger("bandit") + _sarif_filter = _SuppressSarifFilter() + _filtered_handlers = _add_filter_to_handlers(_bandit_root, _sarif_filter) + _bandit_root.addFilter(_sarif_filter) + try: + from bandit.core import manager as _bandit_manager + from bandit.core import config as _bandit_config + finally: + _bandit_root.removeFilter(_sarif_filter) + _remove_filter_from_handlers(_filtered_handlers, _sarif_filter) _HAS_BANDIT = True except ImportError: # pragma: no cover diff --git a/semantic_code_intelligence/cli/commands/hotspots_cmd.py b/semantic_code_intelligence/cli/commands/hotspots_cmd.py index 8a0109f..e85fb5d 100644 --- a/semantic_code_intelligence/cli/commands/hotspots_cmd.py +++ b/semantic_code_intelligence/cli/commands/hotspots_cmd.py @@ -18,11 +18,17 @@ @click.command("hotspots") +@click.argument( + "directory", + default=None, + required=False, + type=click.Path(exists=True, file_okay=False, resolve_path=True), +) @click.option( "--path", "-p", default=".", type=click.Path(exists=True, file_okay=False, resolve_path=True), - help="Project root path.", + help="Project root path (alternative to the positional argument).", ) @click.option( "--json-output", "--json", "json_mode", @@ -47,6 +53,7 @@ @click.pass_context def hotspots_cmd( ctx: click.Context, + directory: str | None, path: str, json_mode: bool, pipe: bool, @@ -62,6 +69,8 @@ def hotspots_cmd( codexa hotspots + codexa hotspots . + codexa hotspots --top-n 10 --json codexa hotspots --no-git --pipe @@ -69,7 +78,11 @@ def hotspots_cmd( from semantic_code_intelligence.ci.hotspots import analyze_hotspots from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap - root = Path(path).resolve() + if directory is not None and ctx.get_parameter_source("path") == click.core.ParameterSource.COMMANDLINE: + raise click.UsageError( + "Provide either the positional 'directory' argument or '--path', not both." + ) + root = Path(directory or path).resolve() builder = ContextBuilder() dep_map = DependencyMap() diff --git a/semantic_code_intelligence/cli/commands/quality_cmd.py b/semantic_code_intelligence/cli/commands/quality_cmd.py index 32387c3..7637c51 100644 --- a/semantic_code_intelligence/cli/commands/quality_cmd.py +++ b/semantic_code_intelligence/cli/commands/quality_cmd.py @@ -112,12 +112,18 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None: @click.command("quality") +@click.argument( + "directory", + default=None, + required=False, + type=click.Path(exists=True, file_okay=False, resolve_path=True), +) @click.option( "--path", "-p", default=".", type=click.Path(exists=True, file_okay=False, resolve_path=True), - help="Project root path.", + help="Project root path (alternative to the positional argument).", ) @click.option( "--json-output", @@ -148,6 +154,7 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None: @click.pass_context def quality_cmd( ctx: click.Context, + directory: str | None, path: str, json_mode: bool, complexity_threshold: int, @@ -163,6 +170,8 @@ def quality_cmd( codexa quality + codexa quality . + codexa quality --json codexa quality --safety-only --pipe @@ -172,8 +181,15 @@ def quality_cmd( from semantic_code_intelligence.ci.quality import analyze_project, QualityReport from semantic_code_intelligence.llm.safety import SafetyValidator - root = Path(path).resolve() + directory_path = Path(directory).resolve() if directory else None + option_path = Path(path).resolve() + + if directory_path is not None and directory_path != option_path: + raise click.UsageError( + "Cannot use both the positional 'directory' argument and '--path' with different values." + ) + root = directory_path or option_path if safety_only: # Fast path: only safety scan from semantic_code_intelligence.parsing.parser import EXTENSION_TO_LANGUAGE