Skip to content
Open
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
3 changes: 3 additions & 0 deletions jscan/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Report a project scale line (Micro, Small, Medium, Large, Enterprise, classified by analyzed file count) directly below the health score in the terminal summary, the text output, and the HTML report header. The `summary` object of the JSON and YAML output gains `project_scale` and `total_loc`. The scale is contextual only and does not affect the health score
- Apply `analysis.include_patterns`, `analysis.recursive`, `complexity.report_unchanged`, `dead_code.min_severity`, `dead_code.sort_by`, and `output.sort_by`, which until now were validated and then ignored. Include patterns select from the file types jscan can parse, using the same matching rules as `exclude_patterns`; a file named directly on the command line is analyzed whether or not it matches
- Warn on stderr, naming each key, when a configuration file sets keys that no command reads. Misspelled keys are reported the same way
- Report directory complexity rollups. The complexity section of the JSON, YAML, and CSV output gains a `by_directory` array with the function count, average and maximum complexity, high-risk count, and average and maximum nesting depth of each directory, and the text output and the HTML complexity tab gain a matching table. Directory paths are relative to the deepest directory that contains every analyzed file
- Report per-module quality hotspots. The analyze JSON and YAML output gains a `module_quality` array joining, per file, its line count, complexity rollups, and dead-code rollups, with the module name from dependency analysis; the text output, the CSV output, and a new Modules tab in the HTML report show the same table. The rollups are taken before `min_complexity` and `min_severity` are applied, so a filtered report still reports what each module carries

### Changed

Expand All @@ -22,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Measure the `nesting_depth` metric, which was reported as 0 for every function because the calculation was never called and, when called, counted a function's control structures instead of measuring how deeply they nest. An `else if` continues the chain its `if` opened, a `catch` clause stays at the level of its `try`, and a nested function is measured on its own
- Count one decision point per `case` label so a `switch` scores like the equivalent `if` chain instead of adding 1 no matter how many cases it has, and report the case count in the previously always-zero `switch_cases` metric. A `default` clause adds nothing, matching `else`. Switch-heavy code (reducers, dispatchers, state machines) now scores higher, so some functions cross the medium/high risk thresholds and complexity scores drop; the code did not get worse, it was under-measured. The same fix corrects a decision point that was lost when a branch sat inside a `try` block, so an `if` inside `try` now adds 1 as it does anywhere else
- Keep the braces of a `switch` body out of its parsed case list, which also stops them from appearing as empty case branches in the control flow graph
- Match `analysis.exclude_patterns` against whole path segments instead of any substring of a file's path, so the default entries `out` and `dist` no longer drop `src/routes/`, `src/layout/`, `src/checkout/`, or `src/utils/distance.ts`. Patterns containing a slash are matched against the path, with `**` spanning any number of directories, and patterns are now evaluated relative to the analyzed path so a parent directory named `build` no longer excludes an entire project. Affected projects will see more files analyzed and therefore different scores
Expand Down
19 changes: 15 additions & 4 deletions jscan/SYNC.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,26 @@ No row currently uses **sync**: every area that was language-independent enough

Out of sync scope, but reference points when considering porting features to jscan:

- Cognitive complexity: `cognitive_complexity.go`, `nesting_depth.go`, `raw_metrics.go`
- Cognitive complexity: `cognitive_complexity.go`, `raw_metrics.go`. (`nesting_depth.go` no longer belongs here: jscan measures nesting depth its own way in `internal/analyzer/complexity.go`, see "Ported with divergences")
- LCOM4: `lcom.go`, `domain/lcom.go` — the union-find (shared-variable + call-edge unions) and risk assessment now live in `core/lcom.ComputeLCOM4` / `AssessRisk` (pyscn Phase 3, PR #673); adopting this in jscan means pulling from `core/lcom` directly, not porting pyscn's original implementation. Python method/attribute extraction (decorator exclusion, `@property` reclassification, ctypes `_fields_`) stays pyscn-local
- DFA (unused-variable detection): `dfa.go`, `dfa_builder.go` — def-use chain construction and reaching-def linking now live in `core/dfa` (pyscn Phase 3, PR #673) via the `RefExtractor`/`ParamExtractor` extension points; Python def/use extraction stays pyscn-local
- DI anti-pattern detection: `di_antipattern_detector.go` plus `di_*.go`, `*_detector.go`, `framework_patterns.go`
- Split similarity-analysis structure (remaining): `structural_similarity.go`, `semantic_similarity.go`, `similarity_analyzer.go`, multi-dimensional `clone_classifier` — Type-1/2 gates are in `core/clone` (jscan adopted); semantic evidence penalties (disjoint literals, missing strong signals, incompatible return categories) now live in `core/semantic.ApplySemanticEvidence` (pyscn Phase 3, PR #673); structural similarity still fully unported
- Re-export resolution: `reexport_resolver.go`
- Improvement suggestions: `domain/suggestion.go`
- MCP server: `mcp/`, `cmd/pyscn-mcp/`
- Module / directory quality rollups: `domain/module_quality.go`, `app/module_quality.go`, `app/directory_complexity.go`, `service/directory_complexity_formatter.go` plus `AnalyzeResponse.ModuleQuality`, `ComplexityResponse.ByDirectory` / `ModuleRollups`, `DeadCodeResponse.ModuleRollups` — new pyscn feature (2026-07-26–27, #686 per-module hotspots + #533/#687 directory rollups) joining per-file complexity, dead-code and module metadata into a `module_quality` array, and aggregating reported complexity per project-root-relative directory into `by_directory`, with text/CSV/HTML tables for both. ~20 files across domain/app/service; rollups are deliberately computed *before* presentation filters (`min_complexity`, dead-code severity), which is why both response types carry a `json:"-"` `ModuleRollups` map. Two of the six module-complexity fields (`AverageCognitiveComplexity`) and both directory nesting fields have no jscan backing metric — jscan computes `NestingDepth` per function but not cognitive complexity. Tracked for jscan as [polyscan#31](https://github.com/ludo-technologies/polyscan/issues/31)
*(Module / directory quality rollups were on this list until [polyscan#31](https://github.com/ludo-technologies/polyscan/issues/31); see "Ported with divergences" below.)*

### Ported with divergences

- **Module / directory quality rollups** (pyscn #686 + #533/#687, ported for jscan in polyscan#31) — jscan has `domain/module_quality.go`, `domain/directory_complexity.go`, `service/module_quality.go`, `service/directory_complexity_formatter.go`, plus `ComplexityResponse.ByDirectory` / `ModuleRollups`, `DeadCodeResponse.ModuleRollups`, and a `module_quality` array on the analyze JSON/YAML output. JSON field names match pyscn. The divergences, all deliberate:
- **No `average_cognitive_complexity`** — cognitive complexity is still unported (see the list above). The field is absent rather than reported as zero
- **No `function_count`** — in pyscn it is the module metadata's function count next to the complexity records' `analyzed_function_count`. jscan's complexity analysis records every function it parses, so the two counts are the same number and only one is reported
- **`lines_of_code` comes from the complexity service**, not from module metadata: it is the only per-file reader that already holds the file content. jscan's dependency analysis leaves `ModuleDependencyMetrics.LinesOfCode` / `FunctionCount` unpopulated, so `module_quality` reads only `ModuleName` from it. Line counting matches pyscn's `countSourceLines`
- **The aggregation lives in the service layer, not the use case** — jscan's `analyze` command calls the services directly, so the use-case seam pyscn joins at does not sit on jscan's main path. `BuildModuleQuality` is a `service` function next to `BuildAnalyzeSummary`, which the analyze command already shares
- **The directory root is the analyzed files' common ancestor**, not the caller-facing path set: the services receive collected file lists, not the caller's paths. Reported paths therefore strip the shared prefix of the selection, and a single-directory run reports one `.` row
- **HTML tables are not sortable** — jscan's report has no table sorting anywhere, and adding it to only these two tables would be inconsistent
- **`nesting_depth` was implemented as part of the port.** `CalculateNestingDepth` existed but was never called and counted control structures instead of measuring depth, so the published per-function `nesting_depth` was always 0. It now measures real depth (else-if chains stay flat, a catch clause stays at its try's level, nested functions are measured separately) and the directory rollups aggregate it
- Module community detection: `internal/analyzer/community_metrics.go`, `domain/community.go`, `app/community_usecase.go`, `service/community_analysis_service.go`, `service/community_formatter.go`, `service/community_config_loader.go` — new pyscn feature (2026-06-22–30, #582/#583/#602) clustering modules into communities (modularity, cross-community edges, bridge modules, package/layer alignment) with an opt-in Health Score penalty (`MaxCommunityPenalty`, max 10). Entirely new subsystem with CLI/config/MCP wiring across ~15 files; not present in jscan at all. The Health Score integration in `domain/analyze.go` only activates when `CommunitiesEnabled` and `CommunityCount >= 2`, so it has no effect on jscan's (unmodified) grade computation as long as the feature stays unported

## Pending changes
Expand Down Expand Up @@ -131,8 +142,8 @@ Skipped during the 2026-07-25 sync (`249b121`..`6af3ee7`):
Skipped during the 2026-07-28 sync (`6af3ee7`..`81b7ec0`):

- **Architecture-suggestion refinements** (`3e7d83c`/#647 dedup + `file_path`/`start_line`, `a146222` preserve distinct violations, `ab8f944` prioritize critical suggestions, `0edce40` identity-dimension tests, `5554baf` per-problem violation merge + shared category cap) — all in `domain/suggestion.go`, which is on the unported-features list ("Improvement suggestions"). jscan has no suggestion subsystem to apply these to; port them together with `domain/suggestion.go` if it is ever adopted
- **Module quality rollups** (`91075ec`, `270eb98`, `ca0c48b`, `ed9602c`, `499a05f`, `5e193e9`, `8c32d21`, `db2de9b`, `6a51bc8`, `0043feb`, plus `fe247fa`/#686 and the unmapped app/service/report commits `3093066`, `77af996`, `52ddb7f`, `9f60bd7`, `9394e7c`, `72cde27`, `df7803f`, `f2b7a3d`, `9487428`, `4c26413`) — new pyscn subsystem; see "pyscn-specific, unported features" and [polyscan#31](https://github.com/ludo-technologies/polyscan/issues/31)
- **Directory quality rollups** (`d985d0f`, `21c8636`, `f0713b7`, `3308c94`, `b6e5e3c`, `be35e03`, `6209e60`, plus the unmapped `39ca5ec`, `5b7226f`, `535c8e6`, `72337d2`, `af33f81`, `d9d8c64`, `ce8d982`, `d39ddef`, `86e5875`, `86d3ed2`, `3dd97bf`, `40dd367` — #533/#687) — second half of the same feature; same tracking issue. Note `d9d8c64` (buffer the analyze text report and propagate a single write error) is a genuinely portable formatter fix, but it exists only to wrap the new directory/module sections — jscan's `AnalyzeFormatter` writes with `fmt.Fprint` throughout and has no equivalent error-propagation seam to fix in isolation
- **Module quality rollups** (`91075ec`, `270eb98`, `ca0c48b`, `ed9602c`, `499a05f`, `5e193e9`, `8c32d21`, `db2de9b`, `6a51bc8`, `0043feb`, plus `fe247fa`/#686 and the unmapped app/service/report commits `3093066`, `77af996`, `52ddb7f`, `9f60bd7`, `9394e7c`, `72cde27`, `df7803f`, `f2b7a3d`, `9487428`, `4c26413`) — new pyscn subsystem; skipped by the sync run and **ported afterwards in [polyscan#31](https://github.com/ludo-technologies/polyscan/issues/31)**, see "Ported with divergences"
- **Directory quality rollups** (`d985d0f`, `21c8636`, `f0713b7`, `3308c94`, `b6e5e3c`, `be35e03`, `6209e60`, plus the unmapped `39ca5ec`, `5b7226f`, `535c8e6`, `72337d2`, `af33f81`, `d9d8c64`, `ce8d982`, `d39ddef`, `86e5875`, `86d3ed2`, `3dd97bf`, `40dd367` — #533/#687) — second half of the same feature; same tracking issue. Note `d9d8c64` (buffer the analyze text report and propagate a single write error) is a genuinely portable formatter fix, but it exists only to wrap the new directory/module sections — jscan's `AnalyzeFormatter` writes with `fmt.Fprint` throughout and has no equivalent error-propagation seam to fix in isolation. The rollups themselves were **ported afterwards in [polyscan#31](https://github.com/ludo-technologies/polyscan/issues/31)**
- **MCP registry listing** (`300e1c9`, `9c3ab20`) — jscan has no MCP server

## Sync history
Expand Down
46 changes: 44 additions & 2 deletions jscan/domain/complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package domain

import (
"context"
"encoding/json"
"io"
)

Expand Down Expand Up @@ -132,11 +133,52 @@ type ComplexitySummary struct {
ComplexityDistribution map[string]int `json:"complexity_distribution,omitempty" yaml:"complexity_distribution,omitempty"`
}

// DirectoryComplexityMetrics aggregates reported ComplexityResponse.Functions
// entries for one project-root-relative directory.
type DirectoryComplexityMetrics struct {
DirectoryPath string `json:"directory_path" yaml:"directory_path"`
FunctionCount int `json:"function_count" yaml:"function_count"`
AverageComplexity float64 `json:"average_complexity" yaml:"average_complexity"`
MaxComplexity int `json:"max_complexity" yaml:"max_complexity"`
HighRiskFunctionCount int `json:"high_risk_function_count" yaml:"high_risk_function_count"`
AverageNestingDepth float64 `json:"average_nesting_depth" yaml:"average_nesting_depth"`
MaxNestingDepth int `json:"max_nesting_depth" yaml:"max_nesting_depth"`
}

// DirectoryComplexityMetricsList is the stable serialized collection contract.
// A zero value is encoded as an empty array so callers never need to distinguish
// an uninitialized collection from a completed analysis with no reported rows.
type DirectoryComplexityMetricsList []DirectoryComplexityMetrics

// MarshalJSON encodes an uninitialized collection as an empty JSON array.
func (metrics DirectoryComplexityMetricsList) MarshalJSON() ([]byte, error) {
if metrics == nil {
return []byte("[]"), nil
}
type plainDirectoryComplexityMetricsList DirectoryComplexityMetricsList
return json.Marshal(plainDirectoryComplexityMetricsList(metrics))
}

// MarshalYAML encodes an uninitialized collection as an empty YAML array.
func (metrics DirectoryComplexityMetricsList) MarshalYAML() (interface{}, error) {
if metrics == nil {
return []DirectoryComplexityMetrics{}, nil
}
return []DirectoryComplexityMetrics(metrics), nil
}

// ComplexityResponse represents the complete analysis result
type ComplexityResponse struct {
// Analysis results
Functions []FunctionComplexity `json:"functions" yaml:"functions"`
Summary ComplexitySummary `json:"summary" yaml:"summary"`
Functions []FunctionComplexity `json:"functions" yaml:"functions"`
ByDirectory DirectoryComplexityMetricsList `json:"by_directory" yaml:"by_directory"`
Summary ComplexitySummary `json:"summary" yaml:"summary"`

// ModuleRollups are derived before the report filters are applied, so they
// describe the whole analyzed population rather than what min/max complexity
// left visible. They are consumed by the unified analyze command and are not
// part of standalone complexity output.
ModuleRollups map[string]ModuleComplexityMetrics `json:"-" yaml:"-"`

// Warnings and issues
Warnings []string `json:"warnings,omitempty" yaml:"warnings,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions jscan/domain/dead_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ type DeadCodeResponse struct {
Files []FileDeadCode `json:"files"`
Summary DeadCodeSummary `json:"summary"`

// ModuleRollups are derived before the severity filter is applied, so they
// describe every finding the detectors produced rather than the subset the
// report shows. They are consumed by the unified analyze command and are not
// part of standalone dead code output.
ModuleRollups map[string]ModuleDeadCodeMetrics `json:"-" yaml:"-"`

// Warnings and issues
Warnings []string `json:"warnings"`
Errors []string `json:"errors"`
Expand Down
Loading