Skip to content

Commit 5555066

Browse files
authored
Merge pull request #14 from nandanadileep/fix/quality-hotspots-sarif-errors
fix: positional path arg for quality/hotspots, deduplicate hotspots, suppress sarif import noise
2 parents 63b9e17 + 3d5fe3d commit 5555066

4 files changed

Lines changed: 99 additions & 7 deletions

File tree

semantic_code_intelligence/ci/hotspots.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,16 @@ def analyze_hotspots(
172172
w_fan_out += extra
173173
w_churn = 0.0
174174

175-
callable_symbols = [s for s in symbols if s.kind in ("function", "method")]
175+
# Deduplicate by (file_path, name) so that re-indexed or multiply-parsed
176+
# symbols don't appear as separate hotspot entries with identical scores.
177+
_seen: set[tuple[str, str]] = set()
178+
callable_symbols = []
179+
for s in symbols:
180+
if s.kind in ("function", "method"):
181+
key = (s.file_path, s.name)
182+
if key not in _seen:
183+
_seen.add(key)
184+
callable_symbols.append(s)
176185

177186
# ── Per-symbol raw metrics ───────────────────────────────────
178187
# Complexity

semantic_code_intelligence/ci/quality.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,62 @@ def detect_duplicates(
354354
# ── Bandit security linting (optional) ───────────────────────────────
355355

356356
try:
357-
from bandit.core import manager as _bandit_manager
358-
from bandit.core import config as _bandit_config
357+
import logging as _logging
358+
359+
# Bandit eagerly loads all its formatters at import time, including a
360+
# SARIF formatter that requires the optional `sarif_om` package. When
361+
# `sarif_om` is absent bandit logs an ERROR that appears on every run
362+
# even when SARIF output was never requested. Suppress that specific
363+
# message during the import so users only see errors that are relevant
364+
# to them.
365+
class _SuppressSarifFilter(_logging.Filter):
366+
def filter(self, record: _logging.LogRecord) -> bool:
367+
return "Could not load 'sarif'" not in record.getMessage()
368+
369+
def _logger_ancestry(logger: _logging.Logger) -> list[_logging.Logger]:
370+
ancestry: list[_logging.Logger] = []
371+
current: _logging.Logger | None = logger
372+
while current is not None:
373+
ancestry.append(current)
374+
if not current.propagate:
375+
break
376+
parent = current.parent
377+
current = parent if isinstance(parent, _logging.Logger) else None
378+
return ancestry
379+
380+
def _add_filter_to_handlers(
381+
logger: _logging.Logger,
382+
log_filter: _logging.Filter,
383+
) -> list[_logging.Handler]:
384+
filtered_handlers: list[_logging.Handler] = []
385+
seen_handlers: set[int] = set()
386+
for current_logger in _logger_ancestry(logger):
387+
for handler in current_logger.handlers:
388+
handler_id = id(handler)
389+
if handler_id in seen_handlers:
390+
continue
391+
handler.addFilter(log_filter)
392+
filtered_handlers.append(handler)
393+
seen_handlers.add(handler_id)
394+
return filtered_handlers
395+
396+
def _remove_filter_from_handlers(
397+
handlers: list[_logging.Handler],
398+
log_filter: _logging.Filter,
399+
) -> None:
400+
for handler in handlers:
401+
handler.removeFilter(log_filter)
402+
403+
_bandit_root = _logging.getLogger("bandit")
404+
_sarif_filter = _SuppressSarifFilter()
405+
_filtered_handlers = _add_filter_to_handlers(_bandit_root, _sarif_filter)
406+
_bandit_root.addFilter(_sarif_filter)
407+
try:
408+
from bandit.core import manager as _bandit_manager
409+
from bandit.core import config as _bandit_config
410+
finally:
411+
_bandit_root.removeFilter(_sarif_filter)
412+
_remove_filter_from_handlers(_filtered_handlers, _sarif_filter)
359413

360414
_HAS_BANDIT = True
361415
except ImportError: # pragma: no cover

semantic_code_intelligence/cli/commands/hotspots_cmd.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818

1919

2020
@click.command("hotspots")
21+
@click.argument(
22+
"directory",
23+
default=None,
24+
required=False,
25+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
26+
)
2127
@click.option(
2228
"--path", "-p",
2329
default=".",
2430
type=click.Path(exists=True, file_okay=False, resolve_path=True),
25-
help="Project root path.",
31+
help="Project root path (alternative to the positional argument).",
2632
)
2733
@click.option(
2834
"--json-output", "--json", "json_mode",
@@ -47,6 +53,7 @@
4753
@click.pass_context
4854
def hotspots_cmd(
4955
ctx: click.Context,
56+
directory: str | None,
5057
path: str,
5158
json_mode: bool,
5259
pipe: bool,
@@ -62,14 +69,20 @@ def hotspots_cmd(
6269
6370
codexa hotspots
6471
72+
codexa hotspots .
73+
6574
codexa hotspots --top-n 10 --json
6675
6776
codexa hotspots --no-git --pipe
6877
"""
6978
from semantic_code_intelligence.ci.hotspots import analyze_hotspots
7079
from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap
7180

72-
root = Path(path).resolve()
81+
if directory is not None and ctx.get_parameter_source("path") == click.core.ParameterSource.COMMANDLINE:
82+
raise click.UsageError(
83+
"Provide either the positional 'directory' argument or '--path', not both."
84+
)
85+
root = Path(directory or path).resolve()
7386
builder = ContextBuilder()
7487
dep_map = DependencyMap()
7588

semantic_code_intelligence/cli/commands/quality_cmd.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,18 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None:
112112

113113

114114
@click.command("quality")
115+
@click.argument(
116+
"directory",
117+
default=None,
118+
required=False,
119+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
120+
)
115121
@click.option(
116122
"--path",
117123
"-p",
118124
default=".",
119125
type=click.Path(exists=True, file_okay=False, resolve_path=True),
120-
help="Project root path.",
126+
help="Project root path (alternative to the positional argument).",
121127
)
122128
@click.option(
123129
"--json-output",
@@ -148,6 +154,7 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None:
148154
@click.pass_context
149155
def quality_cmd(
150156
ctx: click.Context,
157+
directory: str | None,
151158
path: str,
152159
json_mode: bool,
153160
complexity_threshold: int,
@@ -163,6 +170,8 @@ def quality_cmd(
163170
164171
codexa quality
165172
173+
codexa quality .
174+
166175
codexa quality --json
167176
168177
codexa quality --safety-only --pipe
@@ -172,8 +181,15 @@ def quality_cmd(
172181
from semantic_code_intelligence.ci.quality import analyze_project, QualityReport
173182
from semantic_code_intelligence.llm.safety import SafetyValidator
174183

175-
root = Path(path).resolve()
184+
directory_path = Path(directory).resolve() if directory else None
185+
option_path = Path(path).resolve()
186+
187+
if directory_path is not None and directory_path != option_path:
188+
raise click.UsageError(
189+
"Cannot use both the positional 'directory' argument and '--path' with different values."
190+
)
176191

192+
root = directory_path or option_path
177193
if safety_only:
178194
# Fast path: only safety scan
179195
from semantic_code_intelligence.parsing.parser import EXTENSION_TO_LANGUAGE

0 commit comments

Comments
 (0)