Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion semantic_code_intelligence/ci/hotspots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 56 additions & 2 deletions semantic_code_intelligence/ci/quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions semantic_code_intelligence/cli/commands/hotspots_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -47,6 +53,7 @@
@click.pass_context
def hotspots_cmd(
ctx: click.Context,
directory: str | None,
path: str,
json_mode: bool,
pipe: bool,
Expand All @@ -62,14 +69,20 @@ def hotspots_cmd(

codexa hotspots

codexa hotspots .

codexa hotspots --top-n 10 --json

codexa hotspots --no-git --pipe
"""
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()

Expand Down
20 changes: 18 additions & 2 deletions semantic_code_intelligence/cli/commands/quality_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -163,6 +170,8 @@ def quality_cmd(

codexa quality

codexa quality .

codexa quality --json

codexa quality --safety-only --pipe
Expand All @@ -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
Expand Down
Loading