projscan 0.9.1 → 0.10.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 +14 -2
- 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/core/codeGraph.d.ts +6 -7
- package/dist/core/codeGraph.js +48 -72
- package/dist/core/codeGraph.js.map +1 -1
- package/dist/core/fileInspector.d.ts +3 -0
- package/dist/core/fileInspector.js +47 -2
- package/dist/core/fileInspector.js.map +1 -1
- package/dist/core/indexCache.js +5 -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/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 +142 -0
- package/dist/core/languages/pythonAdapter.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 +30 -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 +75 -0
- package/dist/core/languages/treeSitterLoader.js.map +1 -0
- package/dist/core/searchIndex.js +8 -0
- package/dist/core/searchIndex.js.map +1 -1
- package/dist/core/upgradePreview.d.ts +12 -0
- package/dist/core/upgradePreview.js +54 -2
- package/dist/core/upgradePreview.js.map +1 -1
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/web-tree-sitter.wasm +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.js +48 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils/fileWalker.js +14 -0
- package/dist/utils/fileWalker.js.map +1 -1
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -190,10 +190,22 @@ 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 (full AST analysis for all three), plus file-level detection for Go, 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
|
+
|
|
197
209
|
**Issues**:
|
|
198
210
|
- Missing linting (ESLint) and formatting (Prettier) configuration
|
|
199
211
|
- Missing test framework
|
|
@@ -210,7 +222,7 @@ This outputs a [shields.io](https://shields.io) badge URL and markdown snippet y
|
|
|
210
222
|
- **5,000 files** analyzed in under 1.5 seconds
|
|
211
223
|
- **20,000 files** analyzed in under 3 seconds
|
|
212
224
|
- **Zero network requests** - everything runs locally
|
|
213
|
-
- **
|
|
225
|
+
- **6 runtime dependencies** - still minimal footprint (the two tree-sitter packages add ~640 KB of vendored wasm)
|
|
214
226
|
|
|
215
227
|
## CI/CD Integration
|
|
216
228
|
|
|
@@ -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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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const TEST_FRAMEWORKS = ['pytest', 'unittest', 'nose', 'nose2', 'ward'];
|
|
4
|
+
function isPythonTestFile(rel) {
|
|
5
|
+
const base = path.basename(rel);
|
|
6
|
+
if (/^test_.+\.py$/.test(base))
|
|
7
|
+
return true;
|
|
8
|
+
if (/^.+_test\.py$/.test(base))
|
|
9
|
+
return true;
|
|
10
|
+
if (rel.startsWith('tests/') || rel.includes('/tests/'))
|
|
11
|
+
return base.endsWith('.py');
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
async function tryRead(absolutePath) {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.readFile(absolutePath, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function check(rootPath, files) {
|
|
23
|
+
const hasPython = files.some((f) => f.extension === '.py' || f.extension === '.pyw');
|
|
24
|
+
if (!hasPython)
|
|
25
|
+
return [];
|
|
26
|
+
const pyproject = await tryRead(path.join(rootPath, 'pyproject.toml'));
|
|
27
|
+
const setupCfg = await tryRead(path.join(rootPath, 'setup.cfg'));
|
|
28
|
+
const pytestIni = await tryRead(path.join(rootPath, 'pytest.ini'));
|
|
29
|
+
const toxIni = await tryRead(path.join(rootPath, 'tox.ini'));
|
|
30
|
+
// Collect all requirements*.txt contents into one searchable blob.
|
|
31
|
+
const reqRels = files
|
|
32
|
+
.filter((f) => (!f.directory || f.directory === '.') &&
|
|
33
|
+
/^requirements(-.*)?\.txt$/i.test(path.basename(f.relativePath)))
|
|
34
|
+
.map((f) => f.relativePath);
|
|
35
|
+
let requirementsBlob = '';
|
|
36
|
+
for (const rel of reqRels) {
|
|
37
|
+
const content = await tryRead(path.join(rootPath, rel));
|
|
38
|
+
if (content)
|
|
39
|
+
requirementsBlob += content + '\n';
|
|
40
|
+
}
|
|
41
|
+
let hasFramework = false;
|
|
42
|
+
const manifestHaystack = [pyproject ?? '', setupCfg ?? '', requirementsBlob].join('\n');
|
|
43
|
+
for (const fw of TEST_FRAMEWORKS) {
|
|
44
|
+
// Use a word-boundary-ish match so "pytest" doesn't spuriously match "pytest-cov"
|
|
45
|
+
// for purposes of "is pytest the framework?" - but we also accept pytest-*, so
|
|
46
|
+
// a simple case-insensitive containment is fine in practice.
|
|
47
|
+
const re = new RegExp(`(^|[\\s"'\`\\[\\],={}><~^!;])${fw}(\\b|[^a-zA-Z0-9_.-])`, 'im');
|
|
48
|
+
if (re.test(manifestHaystack)) {
|
|
49
|
+
hasFramework = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!hasFramework && pytestIni !== null)
|
|
54
|
+
hasFramework = true;
|
|
55
|
+
if (!hasFramework && toxIni !== null && /\[pytest\]|\[tool:pytest\]|pytest/i.test(toxIni)) {
|
|
56
|
+
hasFramework = true;
|
|
57
|
+
}
|
|
58
|
+
if (!hasFramework && pyproject !== null && /\[tool\.pytest\.ini_options\]/.test(pyproject)) {
|
|
59
|
+
hasFramework = true;
|
|
60
|
+
}
|
|
61
|
+
// Last-resort detection: `import unittest` (or `from unittest ...`) inside a
|
|
62
|
+
// pytest-conventional test file. Some projects rely on stdlib unittest and
|
|
63
|
+
// never declare a framework in manifests.
|
|
64
|
+
const testFiles = files.filter((f) => isPythonTestFile(f.relativePath));
|
|
65
|
+
if (!hasFramework) {
|
|
66
|
+
for (const f of testFiles) {
|
|
67
|
+
const content = await tryRead(f.absolutePath);
|
|
68
|
+
if (content && /^\s*(import\s+unittest|from\s+unittest\s+import)/m.test(content)) {
|
|
69
|
+
hasFramework = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const issues = [];
|
|
75
|
+
if (!hasFramework) {
|
|
76
|
+
issues.push({
|
|
77
|
+
id: 'missing-python-test-framework',
|
|
78
|
+
title: 'No Python test framework detected',
|
|
79
|
+
description: 'No pytest/unittest configuration or dependency found. Testing is essential for code quality and reliability.',
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
category: 'testing',
|
|
82
|
+
fixAvailable: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (testFiles.length === 0) {
|
|
86
|
+
issues.push({
|
|
87
|
+
id: 'no-python-test-files',
|
|
88
|
+
title: 'No Python test files found',
|
|
89
|
+
description: 'A Python test framework is configured but no test files were found (expected test_*.py, *_test.py, or under tests/).',
|
|
90
|
+
severity: 'info',
|
|
91
|
+
category: 'testing',
|
|
92
|
+
fixAvailable: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return issues;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=pythonTestCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pythonTestCheck.js","sourceRoot":"","sources":["../../src/analyzers/pythonTestCheck.ts"],"names":[],"mappings":"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"}
|