projscan 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/dist/analyzers/deadCodeCheck.d.ts +10 -12
- package/dist/analyzers/deadCodeCheck.js +41 -69
- package/dist/analyzers/deadCodeCheck.js.map +1 -1
- package/dist/analyzers/pythonDependencyRiskCheck.d.ts +2 -0
- package/dist/analyzers/pythonDependencyRiskCheck.js +114 -0
- package/dist/analyzers/pythonDependencyRiskCheck.js.map +1 -0
- package/dist/analyzers/pythonLinterCheck.d.ts +2 -0
- package/dist/analyzers/pythonLinterCheck.js +119 -0
- package/dist/analyzers/pythonLinterCheck.js.map +1 -0
- package/dist/analyzers/pythonTestCheck.d.ts +2 -0
- package/dist/analyzers/pythonTestCheck.js +97 -0
- package/dist/analyzers/pythonTestCheck.js.map +1 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.d.ts +2 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.js +76 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.js.map +1 -0
- package/dist/cli/index.js +294 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ast.d.ts +2 -0
- package/dist/core/ast.js +35 -2
- package/dist/core/ast.js.map +1 -1
- package/dist/core/codeGraph.d.ts +8 -7
- package/dist/core/codeGraph.js +50 -72
- package/dist/core/codeGraph.js.map +1 -1
- package/dist/core/couplingAnalyzer.d.ts +18 -0
- package/dist/core/couplingAnalyzer.js +174 -0
- package/dist/core/couplingAnalyzer.js.map +1 -0
- package/dist/core/fileInspector.d.ts +3 -0
- package/dist/core/fileInspector.js +78 -3
- package/dist/core/fileInspector.js.map +1 -1
- package/dist/core/hotspotAnalyzer.d.ts +13 -0
- package/dist/core/hotspotAnalyzer.js +29 -6
- package/dist/core/hotspotAnalyzer.js.map +1 -1
- package/dist/core/indexCache.js +8 -1
- package/dist/core/indexCache.js.map +1 -1
- package/dist/core/issueEngine.js +10 -0
- package/dist/core/issueEngine.js.map +1 -1
- package/dist/core/languages/LanguageAdapter.d.ts +36 -0
- package/dist/core/languages/LanguageAdapter.js +2 -0
- package/dist/core/languages/LanguageAdapter.js.map +1 -0
- package/dist/core/languages/goAdapter.d.ts +2 -0
- package/dist/core/languages/goAdapter.js +136 -0
- package/dist/core/languages/goAdapter.js.map +1 -0
- package/dist/core/languages/goCyclomatic.d.ts +21 -0
- package/dist/core/languages/goCyclomatic.js +55 -0
- package/dist/core/languages/goCyclomatic.js.map +1 -0
- package/dist/core/languages/goExports.d.ts +26 -0
- package/dist/core/languages/goExports.js +89 -0
- package/dist/core/languages/goExports.js.map +1 -0
- package/dist/core/languages/goImports.d.ts +26 -0
- package/dist/core/languages/goImports.js +64 -0
- package/dist/core/languages/goImports.js.map +1 -0
- package/dist/core/languages/goManifests.d.ts +19 -0
- package/dist/core/languages/goManifests.js +56 -0
- package/dist/core/languages/goManifests.js.map +1 -0
- package/dist/core/languages/javascriptAdapter.d.ts +2 -0
- package/dist/core/languages/javascriptAdapter.js +68 -0
- package/dist/core/languages/javascriptAdapter.js.map +1 -0
- package/dist/core/languages/pythonAdapter.d.ts +6 -0
- package/dist/core/languages/pythonAdapter.js +147 -0
- package/dist/core/languages/pythonAdapter.js.map +1 -0
- package/dist/core/languages/pythonCyclomatic.d.ts +18 -0
- package/dist/core/languages/pythonCyclomatic.js +45 -0
- package/dist/core/languages/pythonCyclomatic.js.map +1 -0
- package/dist/core/languages/pythonExports.d.ts +28 -0
- package/dist/core/languages/pythonExports.js +169 -0
- package/dist/core/languages/pythonExports.js.map +1 -0
- package/dist/core/languages/pythonImports.d.ts +22 -0
- package/dist/core/languages/pythonImports.js +104 -0
- package/dist/core/languages/pythonImports.js.map +1 -0
- package/dist/core/languages/pythonManifests.d.ts +34 -0
- package/dist/core/languages/pythonManifests.js +344 -0
- package/dist/core/languages/pythonManifests.js.map +1 -0
- package/dist/core/languages/registry.d.ts +5 -0
- package/dist/core/languages/registry.js +31 -0
- package/dist/core/languages/registry.js.map +1 -0
- package/dist/core/languages/treeSitterLoader.d.ts +14 -0
- package/dist/core/languages/treeSitterLoader.js +76 -0
- package/dist/core/languages/treeSitterLoader.js.map +1 -0
- package/dist/core/monorepo.d.ts +20 -0
- package/dist/core/monorepo.js +270 -0
- package/dist/core/monorepo.js.map +1 -0
- package/dist/core/prDiff.d.ts +43 -0
- package/dist/core/prDiff.js +298 -0
- package/dist/core/prDiff.js.map +1 -0
- package/dist/core/searchIndex.js +8 -0
- package/dist/core/searchIndex.js.map +1 -1
- package/dist/core/telemetry.d.ts +90 -0
- package/dist/core/telemetry.js +199 -0
- package/dist/core/telemetry.js.map +1 -0
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/web-tree-sitter.wasm +0 -0
- package/dist/mcp/server.js +22 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +317 -20
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reporters/consoleReporter.d.ts +4 -1
- package/dist/reporters/consoleReporter.js +113 -0
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/jsonReporter.d.ts +4 -1
- package/dist/reporters/jsonReporter.js +9 -0
- package/dist/reporters/jsonReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.d.ts +4 -1
- package/dist/reporters/markdownReporter.js +103 -3
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/types.d.ts +115 -0
- package/dist/utils/cache.d.ts +3 -0
- package/dist/utils/cache.js +51 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/config.js +10 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/fileWalker.js +14 -0
- package/dist/utils/fileWalker.js.map +1 -1
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
AI coding agents are becoming the primary interface to code. Today, when you ask your agent *"which files implement auth?"* or *"what breaks if I bump React from 18 to 19?"* - it either guesses from names, or it shells out to grep and reads raw output not built for it.
|
|
22
22
|
|
|
23
|
-
**projscan is the first code-intelligence tool built for agents, not for humans.** Your agent gets a fast, AST-accurate, context-budget-aware view of your codebase through
|
|
23
|
+
**projscan is the first code-intelligence tool built for agents, not for humans.** Your agent gets a fast, AST-accurate, context-budget-aware view of your codebase through 17 structured MCP tools. It can query the import graph, find symbol definitions, preview upgrades, rank hotspots, diff structural changes between refs, and surface coupling/cycle hotspots - without loading the file tree into its context.
|
|
24
24
|
|
|
25
25
|
Humans get the same thing through the CLI.
|
|
26
26
|
|
|
@@ -190,10 +190,34 @@ This outputs a [shields.io](https://shields.io) badge URL and markdown snippet y
|
|
|
190
190
|
|
|
191
191
|
## What It Detects
|
|
192
192
|
|
|
193
|
-
**Languages**: TypeScript, JavaScript, Python, Go, Rust, Java, Ruby, C/C++, PHP, Swift, Kotlin, and 20+ more
|
|
193
|
+
**Languages**: TypeScript, JavaScript, Python, and Go (full AST analysis for all four), plus file-level detection for Rust, Java, Ruby, C/C++, PHP, Swift, Kotlin, and 20+ more.
|
|
194
194
|
|
|
195
195
|
**Frameworks**: React, Next.js, Vue, Nuxt, Svelte, Angular, Express, Fastify, NestJS, Vite, Tailwind CSS, Prisma, and more
|
|
196
196
|
|
|
197
|
+
### Python (0.10)
|
|
198
|
+
|
|
199
|
+
Python repos now get the same treatment JS/TS has had since 0.6:
|
|
200
|
+
|
|
201
|
+
- **AST-accurate import graph.** `from pkg.mod import x`, relative imports, `__init__.py` packages, `__all__`. Parsed via tree-sitter-python (wasm, offline).
|
|
202
|
+
- **Python-aware analyzers.** Missing pytest / ruff / black config. Deprecated packages (nose, simplejson, pycrypto). Unused `pyproject.toml` / `requirements.txt` deps. Missing lockfile.
|
|
203
|
+
- **Code search.** BM25 and semantic modes work on `.py` files out of the box.
|
|
204
|
+
- **Hotspots + dead code.** Same scoring as JS/TS, with `__init__.py` and pytest test-file conventions understood.
|
|
205
|
+
- **MCP tools work unchanged.** `projscan_graph`, `projscan_search`, `projscan_doctor`, `projscan_hotspots`, etc. all accept Python projects. Agents can ask "which files import `pkg.core`?" and get an answer in milliseconds.
|
|
206
|
+
|
|
207
|
+
`projscan_upgrade` remains Node-only for now - a Python equivalent (reading pip / poetry metadata) is on the roadmap.
|
|
208
|
+
|
|
209
|
+
### Bundle (0.11)
|
|
210
|
+
|
|
211
|
+
0.11.0 is a multi-theme bundle. Five releases of work shipped under one version:
|
|
212
|
+
|
|
213
|
+
- **Signal Quality (was 0.11)** - the hotspot risk score now uses AST-derived **cyclomatic complexity** instead of a line-count proxy. Per-file CC is exposed via `projscan_file` and the new `projscan_coupling` tool. Coupling metrics (fan-in / fan-out / instability) and **circular-import detection** (Tarjan SCC) ship as a first-class `projscan coupling` command and MCP tool.
|
|
214
|
+
- **PR Native (was 0.12)** - new `projscan_pr_diff` tool returns the **structural** diff between two refs: exports added/removed, imports added/removed, call sites added/removed, ΔCC, Δfan-in. Stands up a temporary git worktree at the base ref to get a clean second graph. What an agent reviewing a PR actually wants to know.
|
|
215
|
+
- **Monorepo (was 0.13)** - workspace detection for npm/yarn workspaces, pnpm-workspace.yaml, and Nx/Turbo/Lerna fallback. New `projscan workspaces` command lists every package; `--package <name>` (and the `package` MCP arg) scope `hotspots` and `coupling` to a single workspace.
|
|
216
|
+
- **Observability (was 0.14)** - **opt-in**, privacy-preserving telemetry. Records only tool name, duration, success, version, timestamp; never source content, paths, or arguments. Off by default; enable via `.projscanrc` `telemetry.enabled` or `PROJSCAN_TELEMETRY=1`. Sink is a local JSONL file you control. New `projscan_telemetry` tool surfaces effective state.
|
|
217
|
+
- **Second Language (was 0.15)** - **Go** via tree-sitter-go. Go files now flow through the same graph / hotspot / coupling / pr-diff pipeline as JS/TS and Python. `go.mod` parsed for module-path resolution; capitalization rule applied for export visibility.
|
|
218
|
+
|
|
219
|
+
Cache version bumped 2 → 3 (CC stored per file). Existing v2 caches are discarded on first 0.11 run and rebuilt automatically.
|
|
220
|
+
|
|
197
221
|
**Issues**:
|
|
198
222
|
- Missing linting (ESLint) and formatting (Prettier) configuration
|
|
199
223
|
- Missing test framework
|
|
@@ -210,7 +234,7 @@ This outputs a [shields.io](https://shields.io) badge URL and markdown snippet y
|
|
|
210
234
|
- **5,000 files** analyzed in under 1.5 seconds
|
|
211
235
|
- **20,000 files** analyzed in under 3 seconds
|
|
212
236
|
- **Zero network requests** - everything runs locally
|
|
213
|
-
- **
|
|
237
|
+
- **9 runtime dependencies** - still minimal (the three tree-sitter packages bring ~850 KB of vendored wasm: web-tree-sitter ~190 KB, tree-sitter-python ~450 KB, tree-sitter-go ~210 KB)
|
|
214
238
|
|
|
215
239
|
## CI/CD Integration
|
|
216
240
|
|
|
@@ -417,17 +441,19 @@ claude mcp add projscan -- npx projscan mcp
|
|
|
417
441
|
- *"What breaks if I bump chalk to 6?"* → `projscan_upgrade { package: "chalk" }`
|
|
418
442
|
- *"Where should I refactor first?"* → `projscan_hotspots`
|
|
419
443
|
|
|
420
|
-
### The
|
|
444
|
+
### The 17 MCP tools
|
|
421
445
|
|
|
422
|
-
**Structural (0.6.0
|
|
446
|
+
**Structural (0.6.0 / 0.11 — agent-native):**
|
|
423
447
|
- **`projscan_graph`** - query the AST-based code graph. Directions: `imports`, `exports`, `importers`, `symbol_defs`, `package_importers`. Millisecond responses on a warm cache.
|
|
424
448
|
- **`projscan_search`** - fast search across `symbols` (exported names), `files` (path substring), or `content` (source substring with line + excerpt). Replaces the temptation to shell out to grep.
|
|
449
|
+
- **`projscan_coupling`** *(0.11)* - per-file fan-in / fan-out / instability + circular-import cycles (Tarjan SCC). Filter by `direction: cycles_only | high_fan_in | high_fan_out`.
|
|
450
|
+
- **`projscan_pr_diff`** *(0.11)* - structural diff between two git refs. Returns added/removed/modified files with explicit lists of exports, imports, and call sites that changed, plus ΔCC and Δfan-in.
|
|
425
451
|
|
|
426
452
|
**Analysis:**
|
|
427
453
|
- `projscan_analyze` - full project report
|
|
428
454
|
- `projscan_doctor` - health score + issues
|
|
429
|
-
- `projscan_hotspots` - risk-ranked files (churn × complexity × issues × ownership × coverage)
|
|
430
|
-
- `projscan_file` - per-file risk + ownership + related issues
|
|
455
|
+
- `projscan_hotspots` - risk-ranked files (churn × **AST cyclomatic complexity** × issues × ownership × coverage; falls back to LOC for non-AST languages)
|
|
456
|
+
- `projscan_file` - per-file risk + ownership + related issues + CC + fan-in/fan-out
|
|
431
457
|
- `projscan_explain` - per-file purpose, imports, exports, smells
|
|
432
458
|
- `projscan_structure` - directory tree
|
|
433
459
|
- `projscan_coverage` - scariest untested files (coverage × hotspots)
|
|
@@ -438,6 +464,10 @@ claude mcp add projscan -- npx projscan mcp
|
|
|
438
464
|
- `projscan_audit` - normalized `npm audit`
|
|
439
465
|
- `projscan_upgrade` - upgrade preview (CHANGELOG + importers, offline)
|
|
440
466
|
|
|
467
|
+
**Workspace + observability (0.11):**
|
|
468
|
+
- `projscan_workspaces` - list monorepo packages (npm/yarn/pnpm/Nx/Turbo/Lerna). Use the `name` as the `package` arg on `projscan_hotspots` / `projscan_coupling` to scope.
|
|
469
|
+
- `projscan_telemetry` - inspect opt-in telemetry state (enabled?, sink path, env override).
|
|
470
|
+
|
|
441
471
|
### Context-window budgeting
|
|
442
472
|
|
|
443
473
|
**Every MCP tool accepts an optional `max_tokens` argument.** Set it and projscan serializes the result, and - if over budget - truncates the largest array field record-by-record until it fits. Responses include a `_budget` sidecar when truncated so your agent knows it got a partial view.
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { FileEntry, Issue } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Flag
|
|
4
|
-
*
|
|
5
|
-
* - utilities that are implemented but never hooked up
|
|
3
|
+
* Flag source files whose exports nothing imports. Language-agnostic: uses the
|
|
4
|
+
* code graph directly, so JS/TS/Python all get the same correctness guarantees.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
6
|
+
* Skipped:
|
|
7
|
+
* - public package entries (main/exports/bin/types in package.json)
|
|
8
|
+
* - test files (JS conventions + pytest `test_*.py` / `*_test.py` / `tests/`)
|
|
9
|
+
* - barrel files (`index.*` for JS, `__init__.py` for Python)
|
|
10
|
+
* - default-only exports (too many framework false positives)
|
|
12
11
|
*
|
|
13
|
-
* False-positive guard: if
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* noise low at the cost of missing some dead exports that live in used files.
|
|
12
|
+
* False-positive guard: if any import resolves to this file, we treat ALL its
|
|
13
|
+
* exports as possibly used - the graph can't always tell which named export
|
|
14
|
+
* got picked up from a barrel.
|
|
17
15
|
*/
|
|
18
16
|
export declare function check(rootPath: string, files: FileEntry[]): Promise<Issue[]>;
|
|
@@ -1,47 +1,36 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
6
|
-
|
|
3
|
+
import { buildCodeGraph } from '../core/codeGraph.js';
|
|
4
|
+
import { getAdapterFor } from '../core/languages/registry.js';
|
|
5
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
6
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts',
|
|
7
|
+
'.py', '.pyw',
|
|
8
|
+
]);
|
|
9
|
+
// Never flag these - they're public API by definition (JS convention).
|
|
7
10
|
const PUBLIC_PATH_PREFIXES = ['src/index', 'index.'];
|
|
11
|
+
// Names (sans extension) that are barrel-equivalents and should never be
|
|
12
|
+
// flagged as dead. `index` for JS/TS, `__init__` for Python packages.
|
|
13
|
+
const BARREL_BASENAMES = new Set(['index', '__init__']);
|
|
8
14
|
/**
|
|
9
|
-
* Flag
|
|
10
|
-
*
|
|
11
|
-
* - utilities that are implemented but never hooked up
|
|
15
|
+
* Flag source files whose exports nothing imports. Language-agnostic: uses the
|
|
16
|
+
* code graph directly, so JS/TS/Python all get the same correctness guarantees.
|
|
12
17
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
+
* Skipped:
|
|
19
|
+
* - public package entries (main/exports/bin/types in package.json)
|
|
20
|
+
* - test files (JS conventions + pytest `test_*.py` / `*_test.py` / `tests/`)
|
|
21
|
+
* - barrel files (`index.*` for JS, `__init__.py` for Python)
|
|
22
|
+
* - default-only exports (too many framework false positives)
|
|
18
23
|
*
|
|
19
|
-
* False-positive guard: if
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* noise low at the cost of missing some dead exports that live in used files.
|
|
24
|
+
* False-positive guard: if any import resolves to this file, we treat ALL its
|
|
25
|
+
* exports as possibly used - the graph can't always tell which named export
|
|
26
|
+
* got picked up from a barrel.
|
|
23
27
|
*/
|
|
24
28
|
export async function check(rootPath, files) {
|
|
25
29
|
const sourceFiles = files.filter((f) => SOURCE_EXTENSIONS.has(f.extension));
|
|
26
30
|
if (sourceFiles.length === 0)
|
|
27
31
|
return [];
|
|
28
32
|
const publicEntries = await loadPublicEntries(rootPath);
|
|
29
|
-
const graph = await
|
|
30
|
-
// Build a set of files that are the target of at least one relative import.
|
|
31
|
-
// A relative import specifier is resolved against its importing file's dir,
|
|
32
|
-
// so we convert each relative specifier into a candidate target path.
|
|
33
|
-
const importedTargets = new Set();
|
|
34
|
-
for (const [importingFile, specifiers] of graph.byFile) {
|
|
35
|
-
const importingDir = path.posix.dirname(importingFile);
|
|
36
|
-
for (const spec of specifiers) {
|
|
37
|
-
if (!spec.startsWith('.'))
|
|
38
|
-
continue;
|
|
39
|
-
const resolved = path.posix.normalize(path.posix.join(importingDir, spec));
|
|
40
|
-
for (const candidate of resolutionCandidates(resolved)) {
|
|
41
|
-
importedTargets.add(candidate);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
33
|
+
const graph = await buildCodeGraph(rootPath, sourceFiles);
|
|
45
34
|
const issues = [];
|
|
46
35
|
for (const file of sourceFiles) {
|
|
47
36
|
if (isTestFile(file.relativePath))
|
|
@@ -50,27 +39,25 @@ export async function check(rootPath, files) {
|
|
|
50
39
|
continue;
|
|
51
40
|
if (isPublicEntry(file.relativePath, publicEntries))
|
|
52
41
|
continue;
|
|
53
|
-
|
|
42
|
+
// Any importer → file is used.
|
|
43
|
+
if ((graph.localImporters.get(file.relativePath)?.size ?? 0) > 0)
|
|
54
44
|
continue;
|
|
55
|
-
|
|
45
|
+
const graphFile = graph.files.get(file.relativePath);
|
|
46
|
+
if (!graphFile || !graphFile.parseOk)
|
|
56
47
|
continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
content = await fs.readFile(file.absolutePath, 'utf-8');
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const exports = extractExports(content).filter((e) => e.type !== 'default' && e.name !== 'default');
|
|
65
|
-
if (exports.length === 0)
|
|
48
|
+
const namedExports = graphFile.exports.filter((e) => e.name !== 'default' && e.kind !== 'default');
|
|
49
|
+
if (namedExports.length === 0)
|
|
66
50
|
continue;
|
|
51
|
+
const adapter = getAdapterFor(file.relativePath);
|
|
52
|
+
const languageLabel = adapter?.id ?? 'file';
|
|
53
|
+
const kindLabel = languageLabel === 'python' ? 'name' : 'export';
|
|
67
54
|
issues.push({
|
|
68
55
|
id: `unused-exports-${file.relativePath}`,
|
|
69
|
-
title: `Unused
|
|
70
|
-
description: `${
|
|
56
|
+
title: `Unused ${kindLabel}s in ${file.relativePath}`,
|
|
57
|
+
description: `${namedExports.length} named ${kindLabel}${namedExports.length === 1 ? '' : 's'} (${namedExports
|
|
71
58
|
.slice(0, 5)
|
|
72
59
|
.map((e) => e.name)
|
|
73
|
-
.join(', ')}${
|
|
60
|
+
.join(', ')}${namedExports.length > 5 ? `, … +${namedExports.length - 5}` : ''}) but nothing in the project imports this file. Dead code or awaiting wiring?`,
|
|
74
61
|
severity: 'info',
|
|
75
62
|
category: 'architecture',
|
|
76
63
|
fixAvailable: false,
|
|
@@ -80,14 +67,19 @@ export async function check(rootPath, files) {
|
|
|
80
67
|
return issues;
|
|
81
68
|
}
|
|
82
69
|
function isTestFile(relativePath) {
|
|
70
|
+
const base = path.basename(relativePath);
|
|
83
71
|
return (relativePath.includes('.test.') ||
|
|
84
72
|
relativePath.includes('.spec.') ||
|
|
85
73
|
relativePath.includes('__tests__') ||
|
|
86
|
-
relativePath.startsWith('tests/')
|
|
74
|
+
relativePath.startsWith('tests/') ||
|
|
75
|
+
relativePath.includes('/tests/') ||
|
|
76
|
+
// pytest conventions
|
|
77
|
+
/^test_.+\.py$/.test(base) ||
|
|
78
|
+
/^.+_test\.py$/.test(base));
|
|
87
79
|
}
|
|
88
80
|
function isBarrelFile(relativePath) {
|
|
89
81
|
const base = path.basename(relativePath, path.extname(relativePath));
|
|
90
|
-
return base
|
|
82
|
+
return BARREL_BASENAMES.has(base);
|
|
91
83
|
}
|
|
92
84
|
function isPublicEntry(relativePath, publicEntries) {
|
|
93
85
|
if (publicEntries.has(relativePath))
|
|
@@ -104,26 +96,6 @@ function stripExtension(p) {
|
|
|
104
96
|
const ext = path.extname(p);
|
|
105
97
|
return ext ? p.slice(0, -ext.length) : p;
|
|
106
98
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Return the set of possible resolution targets for a relative import
|
|
109
|
-
* specifier (already joined+normalized). For './foo' we yield:
|
|
110
|
-
* foo, foo.ts, foo.tsx, foo.js, foo.jsx, foo.mjs, foo.cjs,
|
|
111
|
-
* foo/index.ts, foo/index.tsx, foo/index.js, ...
|
|
112
|
-
*/
|
|
113
|
-
function resolutionCandidates(base) {
|
|
114
|
-
const exts = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
|
|
115
|
-
const out = [base];
|
|
116
|
-
for (const ext of exts) {
|
|
117
|
-
out.push(base + ext);
|
|
118
|
-
out.push(base + '/index' + ext);
|
|
119
|
-
}
|
|
120
|
-
// Handle imports written with an explicit ".js" that actually resolve to a .ts file (ESM+NodeNext)
|
|
121
|
-
if (base.endsWith('.js')) {
|
|
122
|
-
const noJs = base.slice(0, -3);
|
|
123
|
-
out.push(noJs + '.ts', noJs + '.tsx');
|
|
124
|
-
}
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
99
|
async function loadPublicEntries(rootPath) {
|
|
128
100
|
const entries = new Set();
|
|
129
101
|
const pkgPath = path.join(rootPath, 'package.json');
|
|
@@ -143,7 +115,7 @@ async function loadPublicEntries(rootPath) {
|
|
|
143
115
|
collectExports(pkg.exports, entries);
|
|
144
116
|
}
|
|
145
117
|
catch {
|
|
146
|
-
// package.json missing
|
|
118
|
+
// package.json missing - nothing to guard
|
|
147
119
|
}
|
|
148
120
|
return entries;
|
|
149
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deadCodeCheck.js","sourceRoot":"","sources":["../../src/analyzers/deadCodeCheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"deadCodeCheck.js","sourceRoot":"","sources":["../../src/analyzers/deadCodeCheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { detectPythonProject } from '../core/languages/pythonManifests.js';
|
|
2
|
+
// Small, conservative seed lists. Easy to extend later.
|
|
3
|
+
const DEPRECATED_WARNING = new Set([
|
|
4
|
+
'nose',
|
|
5
|
+
'simplejson',
|
|
6
|
+
'pycrypto',
|
|
7
|
+
'mysql-python',
|
|
8
|
+
]);
|
|
9
|
+
const DEPRECATED_INFO = new Set([
|
|
10
|
+
'python-dateutil',
|
|
11
|
+
]);
|
|
12
|
+
const HEAVY_INFO = new Set([
|
|
13
|
+
'pandas',
|
|
14
|
+
'numpy',
|
|
15
|
+
'torch',
|
|
16
|
+
'tensorflow',
|
|
17
|
+
]);
|
|
18
|
+
const DEPRECATION_REASONS = {
|
|
19
|
+
nose: 'nose is retired. Use pytest instead.',
|
|
20
|
+
simplejson: 'simplejson is no longer needed; the stdlib json module is equivalent for most use cases.',
|
|
21
|
+
pycrypto: 'pycrypto is unmaintained. Use pycryptodome as a drop-in replacement.',
|
|
22
|
+
'mysql-python': 'mysql-python is Python 2 only. Use mysqlclient or PyMySQL.',
|
|
23
|
+
'python-dateutil': 'python-dateutil is heavy. Many use cases are now covered by stdlib datetime.',
|
|
24
|
+
};
|
|
25
|
+
const HEAVY_REASONS = {
|
|
26
|
+
pandas: 'pandas is a heavy dependency (~30MB). Reach for it only if you actually need dataframes.',
|
|
27
|
+
numpy: 'numpy is a heavy dependency (~15MB). Only pull it if you need numerical arrays.',
|
|
28
|
+
torch: 'torch is a very heavy dependency. Consider torch-cpu if GPU is not needed.',
|
|
29
|
+
tensorflow: 'tensorflow is a very heavy dependency. Consider tensorflow-cpu for inference-only use.',
|
|
30
|
+
};
|
|
31
|
+
export async function check(rootPath, files) {
|
|
32
|
+
const info = await detectPythonProject(rootPath, files);
|
|
33
|
+
if (!info)
|
|
34
|
+
return [];
|
|
35
|
+
const issues = [];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (const dep of info.declared) {
|
|
38
|
+
const id = `dep-risk-${dep.name}`;
|
|
39
|
+
if (seen.has(id))
|
|
40
|
+
continue;
|
|
41
|
+
if (DEPRECATED_WARNING.has(dep.name)) {
|
|
42
|
+
seen.add(id);
|
|
43
|
+
issues.push({
|
|
44
|
+
id,
|
|
45
|
+
title: `Deprecated Python package: ${dep.name}`,
|
|
46
|
+
description: DEPRECATION_REASONS[dep.name] ?? `${dep.name} is deprecated.`,
|
|
47
|
+
severity: 'error',
|
|
48
|
+
category: 'dependencies',
|
|
49
|
+
fixAvailable: false,
|
|
50
|
+
locations: [{ file: dep.source, line: dep.line }],
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (DEPRECATED_INFO.has(dep.name)) {
|
|
55
|
+
seen.add(id);
|
|
56
|
+
issues.push({
|
|
57
|
+
id,
|
|
58
|
+
title: `Consider alternatives to ${dep.name}`,
|
|
59
|
+
description: DEPRECATION_REASONS[dep.name] ?? `${dep.name} may be avoidable.`,
|
|
60
|
+
severity: 'info',
|
|
61
|
+
category: 'dependencies',
|
|
62
|
+
fixAvailable: false,
|
|
63
|
+
locations: [{ file: dep.source, line: dep.line }],
|
|
64
|
+
});
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (HEAVY_INFO.has(dep.name)) {
|
|
68
|
+
seen.add(id);
|
|
69
|
+
issues.push({
|
|
70
|
+
id,
|
|
71
|
+
title: `Heavy Python dependency: ${dep.name}`,
|
|
72
|
+
description: HEAVY_REASONS[dep.name] ?? `${dep.name} is a heavy dependency.`,
|
|
73
|
+
severity: 'info',
|
|
74
|
+
category: 'dependencies',
|
|
75
|
+
fixAvailable: false,
|
|
76
|
+
locations: [{ file: dep.source, line: dep.line }],
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Wildcard / unpinned: only flag entries that came from a requirements file
|
|
81
|
+
// (pyproject.toml dependency strings without a version spec are common and
|
|
82
|
+
// not a smell). An empty versionSpec from requirements.txt IS a smell.
|
|
83
|
+
if (dep.source.endsWith('.txt') &&
|
|
84
|
+
(dep.versionSpec === '' || dep.versionSpec === '*')) {
|
|
85
|
+
seen.add(id);
|
|
86
|
+
issues.push({
|
|
87
|
+
id,
|
|
88
|
+
title: `Unpinned Python dependency: ${dep.name}`,
|
|
89
|
+
description: `\`${dep.name}\` in ${dep.source} has no version constraint. Pin to a specific version (==X.Y.Z) for reproducible builds.`,
|
|
90
|
+
severity: 'error',
|
|
91
|
+
category: 'dependencies',
|
|
92
|
+
fixAvailable: false,
|
|
93
|
+
locations: [{ file: dep.source, line: dep.line }],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// No-lockfile: only emit if there ARE declared deps AND no lockfile-like
|
|
98
|
+
// thing exists. pyproject-only projects without a lockfile are fine for
|
|
99
|
+
// library-style packages, so this is a warning not an error.
|
|
100
|
+
if (!info.hasLockfile && info.declared.length > 0) {
|
|
101
|
+
const severity = 'warning';
|
|
102
|
+
issues.push({
|
|
103
|
+
id: 'dep-risk-no-python-lockfile',
|
|
104
|
+
title: 'No Python lockfile detected',
|
|
105
|
+
description: 'No lockfile (poetry.lock / Pipfile.lock / pdm.lock / uv.lock / conda-lock.yml) or pinned requirements.txt found. Builds may resolve different versions over time.',
|
|
106
|
+
severity,
|
|
107
|
+
category: 'dependencies',
|
|
108
|
+
fixAvailable: false,
|
|
109
|
+
locations: [{ file: info.manifestFiles[0] ?? 'pyproject.toml' }],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=pythonDependencyRiskCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pythonDependencyRiskCheck.js","sourceRoot":"","sources":["../../src/analyzers/pythonDependencyRiskCheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LINTER_CONFIG_FILES = [
|
|
4
|
+
'ruff.toml',
|
|
5
|
+
'.ruff.toml',
|
|
6
|
+
'.flake8',
|
|
7
|
+
'.pylintrc',
|
|
8
|
+
'pylintrc',
|
|
9
|
+
];
|
|
10
|
+
const FORMATTER_CONFIG_FILES = [
|
|
11
|
+
'.autopep8',
|
|
12
|
+
'.yapfrc',
|
|
13
|
+
'yapf.ini',
|
|
14
|
+
];
|
|
15
|
+
async function tryRead(absolutePath) {
|
|
16
|
+
try {
|
|
17
|
+
return await fs.readFile(absolutePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function check(rootPath, files) {
|
|
24
|
+
const hasPython = files.some((f) => f.extension === '.py' || f.extension === '.pyw');
|
|
25
|
+
if (!hasPython)
|
|
26
|
+
return [];
|
|
27
|
+
const rootBasenames = new Set(files
|
|
28
|
+
.filter((f) => !f.directory || f.directory === '.')
|
|
29
|
+
.map((f) => path.basename(f.relativePath)));
|
|
30
|
+
const pyproject = await tryRead(path.join(rootPath, 'pyproject.toml'));
|
|
31
|
+
const setupCfg = await tryRead(path.join(rootPath, 'setup.cfg'));
|
|
32
|
+
const reqRels = files
|
|
33
|
+
.filter((f) => (!f.directory || f.directory === '.') &&
|
|
34
|
+
/^requirements(-.*)?\.txt$/i.test(path.basename(f.relativePath)))
|
|
35
|
+
.map((f) => f.relativePath);
|
|
36
|
+
let requirementsBlob = '';
|
|
37
|
+
for (const rel of reqRels) {
|
|
38
|
+
const content = await tryRead(path.join(rootPath, rel));
|
|
39
|
+
if (content)
|
|
40
|
+
requirementsBlob += content + '\n';
|
|
41
|
+
}
|
|
42
|
+
const manifestHaystack = [pyproject ?? '', setupCfg ?? '', requirementsBlob].join('\n');
|
|
43
|
+
// ── Linter detection ──
|
|
44
|
+
let hasLinter = false;
|
|
45
|
+
for (const f of LINTER_CONFIG_FILES) {
|
|
46
|
+
if (rootBasenames.has(f)) {
|
|
47
|
+
hasLinter = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!hasLinter && pyproject) {
|
|
52
|
+
if (/\[tool\.ruff\]|\[tool\.flake8\]|\[tool\.pylint\]|\[tool\.pylint\./.test(pyproject)) {
|
|
53
|
+
hasLinter = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!hasLinter && setupCfg) {
|
|
57
|
+
if (/\[flake8\]|\[pylint\]/.test(setupCfg))
|
|
58
|
+
hasLinter = true;
|
|
59
|
+
}
|
|
60
|
+
if (!hasLinter) {
|
|
61
|
+
for (const name of ['ruff', 'flake8', 'pylint', 'pyflakes']) {
|
|
62
|
+
const re = new RegExp(`(^|[\\s"'\`\\[\\],={}><~^!;])${name}(\\b|[^a-zA-Z0-9_.-])`, 'im');
|
|
63
|
+
if (re.test(manifestHaystack)) {
|
|
64
|
+
hasLinter = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── Formatter detection ──
|
|
70
|
+
// ruff also formats (ruff format), so if linter is ruff that satisfies formatter.
|
|
71
|
+
let hasFormatter = false;
|
|
72
|
+
for (const f of FORMATTER_CONFIG_FILES) {
|
|
73
|
+
if (rootBasenames.has(f)) {
|
|
74
|
+
hasFormatter = true;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!hasFormatter && pyproject) {
|
|
79
|
+
if (/\[tool\.black\]|\[tool\.autopep8\]|\[tool\.yapf\]|\[tool\.ruff\.format\]|\[tool\.ruff\]/.test(pyproject)) {
|
|
80
|
+
hasFormatter = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!hasFormatter && setupCfg) {
|
|
84
|
+
if (/\[yapf\]/.test(setupCfg))
|
|
85
|
+
hasFormatter = true;
|
|
86
|
+
}
|
|
87
|
+
if (!hasFormatter) {
|
|
88
|
+
for (const name of ['black', 'ruff', 'autopep8', 'yapf']) {
|
|
89
|
+
const re = new RegExp(`(^|[\\s"'\`\\[\\],={}><~^!;])${name}(\\b|[^a-zA-Z0-9_.-])`, 'im');
|
|
90
|
+
if (re.test(manifestHaystack)) {
|
|
91
|
+
hasFormatter = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const issues = [];
|
|
97
|
+
if (!hasLinter) {
|
|
98
|
+
issues.push({
|
|
99
|
+
id: 'missing-python-linter',
|
|
100
|
+
title: 'No Python linter configured',
|
|
101
|
+
description: 'No ruff / flake8 / pylint configuration or dependency found. A linter catches bugs and enforces style.',
|
|
102
|
+
severity: 'warning',
|
|
103
|
+
category: 'linting',
|
|
104
|
+
fixAvailable: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (!hasFormatter) {
|
|
108
|
+
issues.push({
|
|
109
|
+
id: 'missing-python-formatter',
|
|
110
|
+
title: 'No Python formatter configured',
|
|
111
|
+
description: 'No black / ruff-format / autopep8 / yapf configuration or dependency found. A formatter ensures consistent code style.',
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
category: 'formatting',
|
|
114
|
+
fixAvailable: false,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=pythonLinterCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pythonLinterCheck.js","sourceRoot":"","sources":["../../src/analyzers/pythonLinterCheck.ts"],"names":[],"mappings":"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"}
|