@iris-code/core 0.1.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 +38 -0
- package/dist/config.d.ts +147 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +550 -0
- package/dist/config.js.map +1 -0
- package/dist/csharp/analyser.d.ts +14 -0
- package/dist/csharp/analyser.d.ts.map +1 -0
- package/dist/csharp/analyser.js +436 -0
- package/dist/csharp/analyser.js.map +1 -0
- package/dist/csharp/lexer.d.ts +29 -0
- package/dist/csharp/lexer.d.ts.map +1 -0
- package/dist/csharp/lexer.js +182 -0
- package/dist/csharp/lexer.js.map +1 -0
- package/dist/duplicates.d.ts +53 -0
- package/dist/duplicates.d.ts.map +1 -0
- package/dist/duplicates.js +491 -0
- package/dist/duplicates.js.map +1 -0
- package/dist/fileAnalyser.d.ts +4 -0
- package/dist/fileAnalyser.d.ts.map +1 -0
- package/dist/fileAnalyser.js +8 -0
- package/dist/fileAnalyser.js.map +1 -0
- package/dist/fileNaming.d.ts +68 -0
- package/dist/fileNaming.d.ts.map +1 -0
- package/dist/fileNaming.js +343 -0
- package/dist/fileNaming.js.map +1 -0
- package/dist/gate.d.ts +49 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +242 -0
- package/dist/gate.js.map +1 -0
- package/dist/go/analyser.d.ts +5 -0
- package/dist/go/analyser.d.ts.map +1 -0
- package/dist/go/analyser.js +576 -0
- package/dist/go/analyser.js.map +1 -0
- package/dist/guards.d.ts +16 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +355 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/languages.d.ts +46 -0
- package/dist/languages.d.ts.map +1 -0
- package/dist/languages.js +155 -0
- package/dist/languages.js.map +1 -0
- package/dist/lines.d.ts +9 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +58 -0
- package/dist/lines.js.map +1 -0
- package/dist/python/analyser.d.ts +5 -0
- package/dist/python/analyser.d.ts.map +1 -0
- package/dist/python/analyser.js +630 -0
- package/dist/python/analyser.js.map +1 -0
- package/dist/qualitySignals.d.ts +30 -0
- package/dist/qualitySignals.d.ts.map +1 -0
- package/dist/qualitySignals.js +101 -0
- package/dist/qualitySignals.js.map +1 -0
- package/dist/ruby/analyser.d.ts +5 -0
- package/dist/ruby/analyser.d.ts.map +1 -0
- package/dist/ruby/analyser.js +450 -0
- package/dist/ruby/analyser.js.map +1 -0
- package/dist/scoring.d.ts +4 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +37 -0
- package/dist/scoring.js.map +1 -0
- package/dist/secrets.d.ts +5 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +245 -0
- package/dist/secrets.js.map +1 -0
- package/dist/securityContext.d.ts +2 -0
- package/dist/securityContext.d.ts.map +1 -0
- package/dist/securityContext.js +16 -0
- package/dist/securityContext.js.map +1 -0
- package/dist/sfc.d.ts +32 -0
- package/dist/sfc.d.ts.map +1 -0
- package/dist/sfc.js +228 -0
- package/dist/sfc.js.map +1 -0
- package/dist/slop.d.ts +37 -0
- package/dist/slop.d.ts.map +1 -0
- package/dist/slop.js +81 -0
- package/dist/slop.js.map +1 -0
- package/dist/suppressions.d.ts +19 -0
- package/dist/suppressions.d.ts.map +1 -0
- package/dist/suppressions.js +152 -0
- package/dist/suppressions.js.map +1 -0
- package/dist/ts/analyser.d.ts +4 -0
- package/dist/ts/analyser.d.ts.map +1 -0
- package/dist/ts/analyser.js +769 -0
- package/dist/ts/analyser.js.map +1 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractPyThirdPartyImports = extractPyThirdPartyImports;
|
|
4
|
+
exports.analysePython = analysePython;
|
|
5
|
+
const lines_1 = require("../lines");
|
|
6
|
+
const secrets_1 = require("../secrets");
|
|
7
|
+
const securityContext_1 = require("../securityContext");
|
|
8
|
+
const guards_1 = require("../guards");
|
|
9
|
+
const scoring_1 = require("../scoring");
|
|
10
|
+
// --- regex constants ---
|
|
11
|
+
// Top-level and method defs: def func_name(
|
|
12
|
+
const PY_FUNC_RE = /^([ \t]*)def\s+([A-Za-z_]\w*)\s*\(/gm;
|
|
13
|
+
// --- Python stdlib module names (root segments) ---
|
|
14
|
+
const PY_STDLIB = new Set([
|
|
15
|
+
'__future__', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast',
|
|
16
|
+
'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64',
|
|
17
|
+
'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'calendar',
|
|
18
|
+
'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop',
|
|
19
|
+
'collections', 'colorsys', 'compileall', 'concurrent', 'configparser',
|
|
20
|
+
'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'csv',
|
|
21
|
+
'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal',
|
|
22
|
+
'difflib', 'dis', 'doctest', 'email', 'encodings', 'enum', 'errno',
|
|
23
|
+
'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions',
|
|
24
|
+
'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob',
|
|
25
|
+
'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'idlelib',
|
|
26
|
+
'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress',
|
|
27
|
+
'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale',
|
|
28
|
+
'logging', 'lzma', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes',
|
|
29
|
+
'mmap', 'modulefinder', 'multiprocessing', 'netrc', 'nis', 'nntplib',
|
|
30
|
+
'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'pathlib',
|
|
31
|
+
'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform',
|
|
32
|
+
'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile',
|
|
33
|
+
'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue',
|
|
34
|
+
'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter',
|
|
35
|
+
'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex',
|
|
36
|
+
'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket',
|
|
37
|
+
'socketserver', 'spwd', 'sqlite3', 'sre_compile', 'sre_constants',
|
|
38
|
+
'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep',
|
|
39
|
+
'struct', 'subprocess', 'sunau', 'symtable', 'sys', 'sysconfig',
|
|
40
|
+
'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios',
|
|
41
|
+
'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token',
|
|
42
|
+
'tokenize', 'tomllib', 'trace', 'traceback', 'tracemalloc', 'tty',
|
|
43
|
+
'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest',
|
|
44
|
+
'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref',
|
|
45
|
+
'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml',
|
|
46
|
+
'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo',
|
|
47
|
+
]);
|
|
48
|
+
// --- zeroed TS metrics (not applicable to Python) ---
|
|
49
|
+
const ZEROED_TS_METRICS = {
|
|
50
|
+
anyUsages: [],
|
|
51
|
+
tsIgnoreCount: 0,
|
|
52
|
+
tsExpectErrorCount: 0,
|
|
53
|
+
nonNullAssertions: 0,
|
|
54
|
+
typeAssertions: 0,
|
|
55
|
+
missingReturnTypes: 0,
|
|
56
|
+
};
|
|
57
|
+
// --- helpers ---
|
|
58
|
+
// Replace # comment text with spaces (preserving newlines and string contents)
|
|
59
|
+
// so that `def` inside a comment is not matched by PY_FUNC_RE.
|
|
60
|
+
function stripPythonComments(source) {
|
|
61
|
+
const lines = source.split('\n');
|
|
62
|
+
return lines.map(line => {
|
|
63
|
+
let inStr = null;
|
|
64
|
+
let result = '';
|
|
65
|
+
let i = 0;
|
|
66
|
+
while (i < line.length) {
|
|
67
|
+
const ch = line[i];
|
|
68
|
+
if (inStr) {
|
|
69
|
+
result += ch;
|
|
70
|
+
if (ch === '\\') {
|
|
71
|
+
result += line[i + 1] ?? '';
|
|
72
|
+
i += 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === inStr)
|
|
76
|
+
inStr = null;
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
else if (ch === '#') {
|
|
80
|
+
result += ' '.repeat(line.length - i);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
else if (ch === '"' || ch === "'") {
|
|
84
|
+
inStr = ch;
|
|
85
|
+
result += ch;
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
result += ch;
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}).join('\n');
|
|
95
|
+
}
|
|
96
|
+
function stripPythonStringsPreserveNewlines(source) {
|
|
97
|
+
let result = '';
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < source.length) {
|
|
100
|
+
const triple = source.slice(i, i + 3);
|
|
101
|
+
if (triple === '"""' || triple === "'''") {
|
|
102
|
+
result += ' ';
|
|
103
|
+
i += 3;
|
|
104
|
+
while (i < source.length) {
|
|
105
|
+
const currentTriple = source.slice(i, i + 3);
|
|
106
|
+
if (currentTriple === triple) {
|
|
107
|
+
result += ' ';
|
|
108
|
+
i += 3;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (source[i] === '\n')
|
|
112
|
+
result += '\n';
|
|
113
|
+
else
|
|
114
|
+
result += ' ';
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const ch = source[i];
|
|
120
|
+
if (ch !== '"' && ch !== "'") {
|
|
121
|
+
result += ch;
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
result += ' ';
|
|
126
|
+
i++;
|
|
127
|
+
while (i < source.length) {
|
|
128
|
+
const current = source[i];
|
|
129
|
+
if (current === '\n') {
|
|
130
|
+
result += '\n';
|
|
131
|
+
i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (current === '\\') {
|
|
135
|
+
result += ' ';
|
|
136
|
+
if (i + 1 < source.length) {
|
|
137
|
+
result += source[i + 1] === '\n' ? '\n' : ' ';
|
|
138
|
+
}
|
|
139
|
+
i += 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
result += ' ';
|
|
143
|
+
i++;
|
|
144
|
+
if (current === ch)
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
// Keeps only regex SOURCE - the string literal passed to an `re.*` call - and
|
|
151
|
+
// blanks everything else (newlines preserved so offsets stay exact). The ReDoS
|
|
152
|
+
// heuristic runs over this so ordinary arithmetic `(a+b)*c` is never mistaken
|
|
153
|
+
// for a nested-quantifier regex. Python has no regex literals, so only re.*
|
|
154
|
+
// call arguments count; an optional string prefix (r/b/u/f) is allowed.
|
|
155
|
+
const PY_REGEX_CALL_RE = /re\.(?:compile|match|search|fullmatch|sub|subn|findall|finditer|split)\s*\(\s*[rbufRBUF]*$/;
|
|
156
|
+
function pyRegexOnlySource(source) {
|
|
157
|
+
const out = [];
|
|
158
|
+
const n = source.length;
|
|
159
|
+
let i = 0;
|
|
160
|
+
const blank = (ch) => (ch === '\n' ? '\n' : ' ');
|
|
161
|
+
while (i < n) {
|
|
162
|
+
const ch = source[i];
|
|
163
|
+
if (ch === '#') {
|
|
164
|
+
while (i < n && source[i] !== '\n') {
|
|
165
|
+
out.push(' ');
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const triple = source.slice(i, i + 3);
|
|
171
|
+
if (triple === '"""' || triple === "'''") {
|
|
172
|
+
const isRegexArg = PY_REGEX_CALL_RE.test(source.slice(Math.max(0, i - 48), i));
|
|
173
|
+
out.push(' ');
|
|
174
|
+
i += 3;
|
|
175
|
+
while (i < n) {
|
|
176
|
+
if (source.slice(i, i + 3) === triple) {
|
|
177
|
+
out.push(' ');
|
|
178
|
+
i += 3;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
out.push(isRegexArg ? (source[i] === '\n' ? '\n' : source[i]) : blank(source[i]));
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (ch === '"' || ch === "'") {
|
|
187
|
+
const q = ch;
|
|
188
|
+
const isRegexArg = PY_REGEX_CALL_RE.test(source.slice(Math.max(0, i - 48), i));
|
|
189
|
+
out.push(' ');
|
|
190
|
+
i++;
|
|
191
|
+
while (i < n) {
|
|
192
|
+
const c = source[i];
|
|
193
|
+
if (c === '\n') {
|
|
194
|
+
out.push('\n');
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (c === '\\') {
|
|
199
|
+
out.push(isRegexArg ? c : ' ');
|
|
200
|
+
if (i + 1 < n)
|
|
201
|
+
out.push(isRegexArg ? source[i + 1] : blank(source[i + 1]));
|
|
202
|
+
i += 2;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (c === q) {
|
|
206
|
+
out.push(' ');
|
|
207
|
+
i++;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
out.push(isRegexArg ? c : ' ');
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
out.push(blank(ch));
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
return out.join('');
|
|
219
|
+
}
|
|
220
|
+
function findPythonFunctionEnd(lines, startLine) {
|
|
221
|
+
// startLine is 1-indexed; determine indentation of the def line
|
|
222
|
+
const defLine = lines[startLine - 1] ?? '';
|
|
223
|
+
const baseIndent = defLine.length - defLine.trimStart().length;
|
|
224
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
const trimmed = line.trimStart();
|
|
227
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
228
|
+
continue; // blank / comment
|
|
229
|
+
const indent = line.length - trimmed.length;
|
|
230
|
+
if (indent <= baseIndent)
|
|
231
|
+
return i + 1; // 1-indexed
|
|
232
|
+
}
|
|
233
|
+
return lines.length;
|
|
234
|
+
}
|
|
235
|
+
// --- import detection ---
|
|
236
|
+
function isPyThirdParty(module) {
|
|
237
|
+
const root = module.split('.')[0];
|
|
238
|
+
return !PY_STDLIB.has(root) && root !== '' && !root.startsWith('_');
|
|
239
|
+
}
|
|
240
|
+
function extractPyThirdPartyImports(source) {
|
|
241
|
+
const imports = new Set();
|
|
242
|
+
// import foo, import foo as bar, import foo.bar, import foo, bar
|
|
243
|
+
// Single-line match only: allowing \s in the module class let consecutive
|
|
244
|
+
// `import x` lines collapse into one match, so files with the idiomatic
|
|
245
|
+
// one-import-per-line style counted as a single import (found by the 5.11
|
|
246
|
+
// benchmark corpus, fixed 2026-07-19).
|
|
247
|
+
const importRe = /^import[ \t]+([\w.]+(?:[ \t]*,[ \t]*[\w.]+)*(?:[ \t]+as[ \t]+\w+)?)/gm;
|
|
248
|
+
importRe.lastIndex = 0;
|
|
249
|
+
let m;
|
|
250
|
+
while ((m = importRe.exec(source)) !== null) {
|
|
251
|
+
for (const part of m[1].split(',')) {
|
|
252
|
+
const mod = part.trim().split(/\s+as\s+/)[0].trim();
|
|
253
|
+
const root = mod.split('.')[0];
|
|
254
|
+
if (root && isPyThirdParty(mod))
|
|
255
|
+
imports.add(root);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// from foo import bar, from foo.bar import baz
|
|
259
|
+
const fromRe = /^from\s+([\w.]+)\s+import\s+/gm;
|
|
260
|
+
fromRe.lastIndex = 0;
|
|
261
|
+
let fm;
|
|
262
|
+
while ((fm = fromRe.exec(source)) !== null) {
|
|
263
|
+
const mod = fm[1].replace(/^\.+/, ''); // strip relative dot prefix
|
|
264
|
+
const root = mod.split('.')[0];
|
|
265
|
+
if (root && isPyThirdParty(mod))
|
|
266
|
+
imports.add(root);
|
|
267
|
+
}
|
|
268
|
+
return [...imports].sort();
|
|
269
|
+
}
|
|
270
|
+
// --- function detection ---
|
|
271
|
+
function detectPyFunctions(source) {
|
|
272
|
+
const stripped = stripPythonComments(source);
|
|
273
|
+
const results = [];
|
|
274
|
+
const seen = new Set();
|
|
275
|
+
PY_FUNC_RE.lastIndex = 0;
|
|
276
|
+
let match;
|
|
277
|
+
while ((match = PY_FUNC_RE.exec(stripped)) !== null) {
|
|
278
|
+
const name = match[2];
|
|
279
|
+
const line = (0, lines_1.lineOf)(source, match.index);
|
|
280
|
+
const key = `${name}:${line}`;
|
|
281
|
+
if (!seen.has(key)) {
|
|
282
|
+
seen.add(key);
|
|
283
|
+
results.push({ name, line });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return results.sort((a, b) => a.line - b.line);
|
|
287
|
+
}
|
|
288
|
+
// --- helpers ---
|
|
289
|
+
function extractParamInner(lines, startLineIdx) {
|
|
290
|
+
let collected = '';
|
|
291
|
+
let depth = 0;
|
|
292
|
+
let started = false;
|
|
293
|
+
for (let i = startLineIdx; i < lines.length; i++) {
|
|
294
|
+
const line = lines[i];
|
|
295
|
+
for (const ch of line) {
|
|
296
|
+
if (ch === '(') {
|
|
297
|
+
if (!started) {
|
|
298
|
+
started = true;
|
|
299
|
+
depth = 1;
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
depth++;
|
|
303
|
+
}
|
|
304
|
+
else if (ch === ')' && started) {
|
|
305
|
+
depth--;
|
|
306
|
+
if (depth === 0)
|
|
307
|
+
return collected;
|
|
308
|
+
else
|
|
309
|
+
collected += ch;
|
|
310
|
+
}
|
|
311
|
+
else if (started) {
|
|
312
|
+
collected += ch;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (started)
|
|
316
|
+
collected += ' ';
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
// --- code smells ---
|
|
321
|
+
function detectPyCodeSmells(source, functions, lineCount, config, isTest = false) {
|
|
322
|
+
const lines = source.split('\n');
|
|
323
|
+
// Python debug prints — print(...) and pprint(...)
|
|
324
|
+
const consoleLogs = [];
|
|
325
|
+
if (config.enableConsoleLogWarnings) {
|
|
326
|
+
// Scan comment- AND string-stripped source so `print(...)` inside a `#`
|
|
327
|
+
// comment or a triple-quoted docstring is not flagged (the 5.11 benchmark FP
|
|
328
|
+
// console-log/negative/docstring_print.py). A real print call keeps its
|
|
329
|
+
// `print(` outside the string, so detection is unaffected. Both strippers
|
|
330
|
+
// preserve offsets and newlines, so lineOf stays accurate.
|
|
331
|
+
const printSource = stripPythonStringsPreserveNewlines(stripPythonComments(source));
|
|
332
|
+
const printRe = /\bprint\s*\(/g;
|
|
333
|
+
printRe.lastIndex = 0;
|
|
334
|
+
let pm;
|
|
335
|
+
while ((pm = printRe.exec(printSource)) !== null) {
|
|
336
|
+
consoleLogs.push({ line: (0, lines_1.lineOf)(source, pm.index) });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// TODOs — Python uses # comments
|
|
340
|
+
const todos = [];
|
|
341
|
+
if (config.enableTodoDetection) {
|
|
342
|
+
const todoRe = /#.*\b(TODO|FIXME|HACK)\b[:\s]*(.*)/;
|
|
343
|
+
lines.forEach((line, i) => {
|
|
344
|
+
const m = todoRe.exec(line);
|
|
345
|
+
if (m)
|
|
346
|
+
todos.push({ text: `${m[1]}: ${m[2].trim()}`.trim(), line: i + 1 });
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Deep nesting (indentation ≥ 16 spaces — Python uses 4-space indent by convention)
|
|
350
|
+
let deepNesting = 0;
|
|
351
|
+
for (let i = 0; i < functions.length; i++) {
|
|
352
|
+
const startIdx = functions[i].line;
|
|
353
|
+
const endIdx = (functions[i + 1]?.line ?? lineCount + 1) - 1;
|
|
354
|
+
const body = lines.slice(startIdx, endIdx);
|
|
355
|
+
const maxIndent = body.reduce((max, l) => {
|
|
356
|
+
const t = l.trimStart();
|
|
357
|
+
return t && !t.startsWith('#') ? Math.max(max, l.length - t.length) : max;
|
|
358
|
+
}, 0);
|
|
359
|
+
if (maxIndent >= 16)
|
|
360
|
+
deepNesting++;
|
|
361
|
+
}
|
|
362
|
+
// Long parameter lists
|
|
363
|
+
const longParameterLists = [];
|
|
364
|
+
if (config.enableLongParamDetection) {
|
|
365
|
+
for (const fn of functions) {
|
|
366
|
+
const inner = extractParamInner(lines, fn.line - 1);
|
|
367
|
+
if (inner) {
|
|
368
|
+
// Filter out 'self' and 'cls' which are not real parameters
|
|
369
|
+
const params = inner.split(',').filter(p => {
|
|
370
|
+
const name = p.trim().split(/[=:]/)[0].trim();
|
|
371
|
+
return name !== 'self' && name !== 'cls' && name !== '';
|
|
372
|
+
});
|
|
373
|
+
if (params.length >= config.maxParameterCount) {
|
|
374
|
+
longParameterLists.push({ name: fn.name, paramCount: params.length, line: fn.line });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Magic numbers
|
|
380
|
+
const magicNumbers = [];
|
|
381
|
+
if (config.enableMagicNumberDetection) {
|
|
382
|
+
// Matches named assignments that should not be flagged:
|
|
383
|
+
// X = 14, X: int = 14 (module/class-level constant)
|
|
384
|
+
// self.x = 14, cls.attr = 14 (instance/class attribute assignment)
|
|
385
|
+
const namedAssignRe = /^\s*(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*(?:\s*:\s*\w+)?\s*=\s*-?[\d.]+\s*$/;
|
|
386
|
+
const seen = new Set();
|
|
387
|
+
const sanitizedLines = stripPythonStringsPreserveNewlines(stripPythonComments(source)).split('\n');
|
|
388
|
+
sanitizedLines.forEach((stripped, i) => {
|
|
389
|
+
if (namedAssignRe.test(stripped))
|
|
390
|
+
return;
|
|
391
|
+
const numRe = /\b(\d+(?:\.\d+)?)\b/g;
|
|
392
|
+
let nm;
|
|
393
|
+
while ((nm = numRe.exec(stripped)) !== null) {
|
|
394
|
+
const n = nm[1];
|
|
395
|
+
const key = `${i + 1}:${n}`;
|
|
396
|
+
if (n !== '0' && n !== '1' && !seen.has(key)) {
|
|
397
|
+
magicNumbers.push({ value: n, line: i + 1 });
|
|
398
|
+
seen.add(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// --- unused vars and functions ---
|
|
404
|
+
const { unusedVars, unusedFunctions } = config.enableUnusedDetection
|
|
405
|
+
? detectPyUnusedDeclarations(source, functions)
|
|
406
|
+
: { unusedVars: [], unusedFunctions: [] };
|
|
407
|
+
// --- security smells ---
|
|
408
|
+
const evalUsage = [];
|
|
409
|
+
const sqlConcatenation = [];
|
|
410
|
+
const insecureRandom = [];
|
|
411
|
+
const unsafeRegex = [];
|
|
412
|
+
const hardcodedLocalhost = [];
|
|
413
|
+
const disabledTlsVerification = [];
|
|
414
|
+
const debugFlagsEnabled = [];
|
|
415
|
+
const weakHashing = [];
|
|
416
|
+
const openRedirect = [];
|
|
417
|
+
if (config.enableSecuritySmells) {
|
|
418
|
+
const stripped = stripPythonComments(source);
|
|
419
|
+
const evalRe = /\beval\s*\(|\bexec\s*\(/g;
|
|
420
|
+
evalRe.lastIndex = 0;
|
|
421
|
+
let ev;
|
|
422
|
+
while ((ev = evalRe.exec(stripped)) !== null) {
|
|
423
|
+
evalUsage.push({ line: (0, lines_1.lineOf)(source, ev.index) });
|
|
424
|
+
}
|
|
425
|
+
const sqlRe = /['"][^'"\n]*\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^'"\n]*['"]\s*\+/g;
|
|
426
|
+
sqlRe.lastIndex = 0;
|
|
427
|
+
let sq;
|
|
428
|
+
while ((sq = sqlRe.exec(source)) !== null) {
|
|
429
|
+
sqlConcatenation.push({ line: (0, lines_1.lineOf)(source, sq.index) });
|
|
430
|
+
}
|
|
431
|
+
// f-string SQL injection: f"SELECT ... {var}"
|
|
432
|
+
const sqlFstrRe = /f['"][^'"\n]*\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^'"\n]*\{[^}]+\}/g;
|
|
433
|
+
sqlFstrRe.lastIndex = 0;
|
|
434
|
+
let sqf;
|
|
435
|
+
while ((sqf = sqlFstrRe.exec(source)) !== null) {
|
|
436
|
+
sqlConcatenation.push({ line: (0, lines_1.lineOf)(source, sqf.index) });
|
|
437
|
+
}
|
|
438
|
+
const rngRe = /\brandom\.(?:random|randint|randrange|choice|shuffle|sample)\s*\(/g;
|
|
439
|
+
rngRe.lastIndex = 0;
|
|
440
|
+
let rn;
|
|
441
|
+
while ((rn = rngRe.exec(stripped)) !== null) {
|
|
442
|
+
if ((0, securityContext_1.hasSecurityContext)(stripped, rn.index)) {
|
|
443
|
+
insecureRandom.push({ line: (0, lines_1.lineOf)(source, rn.index) });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Confine the nested-quantifier heuristic to regex source (re.* call args)
|
|
447
|
+
// so arithmetic like `(a+b)*c` is never flagged.
|
|
448
|
+
const regexSource = pyRegexOnlySource(source);
|
|
449
|
+
const reRe = /\([^)\n]*[+*][^)\n]*\)[+*]/g;
|
|
450
|
+
reRe.lastIndex = 0;
|
|
451
|
+
let rx;
|
|
452
|
+
while ((rx = reRe.exec(regexSource)) !== null) {
|
|
453
|
+
unsafeRegex.push({ line: (0, lines_1.lineOf)(source, rx.index) });
|
|
454
|
+
}
|
|
455
|
+
if (!isTest) {
|
|
456
|
+
const localRe = /\blocalhost\b|127\.0\.0\.1/g;
|
|
457
|
+
localRe.lastIndex = 0;
|
|
458
|
+
let lh;
|
|
459
|
+
while ((lh = localRe.exec(stripped)) !== null) {
|
|
460
|
+
hardcodedLocalhost.push({ line: (0, lines_1.lineOf)(source, lh.index) });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const tlsRe = /\bverify\s*=\s*False\b/g;
|
|
464
|
+
tlsRe.lastIndex = 0;
|
|
465
|
+
let tl;
|
|
466
|
+
while ((tl = tlsRe.exec(stripped)) !== null) {
|
|
467
|
+
disabledTlsVerification.push({ line: (0, lines_1.lineOf)(source, tl.index) });
|
|
468
|
+
}
|
|
469
|
+
if (!isTest) {
|
|
470
|
+
const dbgRe = /\bdebug\s*=\s*True\b/g;
|
|
471
|
+
dbgRe.lastIndex = 0;
|
|
472
|
+
let db;
|
|
473
|
+
while ((db = dbgRe.exec(stripped)) !== null) {
|
|
474
|
+
debugFlagsEnabled.push({ line: (0, lines_1.lineOf)(source, db.index) });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const hashRe = /\bhashlib\.(?:md5|sha1)\s*\(/g;
|
|
478
|
+
hashRe.lastIndex = 0;
|
|
479
|
+
let wh;
|
|
480
|
+
while ((wh = hashRe.exec(stripped)) !== null) {
|
|
481
|
+
weakHashing.push({ line: (0, lines_1.lineOf)(source, wh.index) });
|
|
482
|
+
}
|
|
483
|
+
const redirectRe = /\bredirect\s*\([^)]*(?:request\.(?:args|form|values|json)|request\[)/g;
|
|
484
|
+
redirectRe.lastIndex = 0;
|
|
485
|
+
let or;
|
|
486
|
+
while ((or = redirectRe.exec(stripped)) !== null) {
|
|
487
|
+
openRedirect.push({ line: (0, lines_1.lineOf)(source, or.index) });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { consoleLogs, todos, deepNesting, longParameterLists, magicNumbers, unusedVars, unusedFunctions, hardcodedSecrets: [], bareSuppressions: [], duplicateBlocks: [], fileNamingViolations: [], evalUsage, sqlConcatenation, insecureRandom, unsafeRegex, hardcodedLocalhost, disabledTlsVerification, debugFlagsEnabled, weakHashing, openRedirect };
|
|
491
|
+
}
|
|
492
|
+
function detectPyUnusedDeclarations(source, functions) {
|
|
493
|
+
const lines = source.split('\n');
|
|
494
|
+
const unusedVars = [];
|
|
495
|
+
const unusedFunctions = [];
|
|
496
|
+
// --- unused variables (function-local only: indented lines) ---
|
|
497
|
+
// Matches: <indent> name = <non-equals> (excludes ==, +=, self.x =, etc.)
|
|
498
|
+
// The ^[ \t]+ requirement naturally excludes module-level (non-indented) assignments.
|
|
499
|
+
const assignRe = /^([ \t]+)([A-Za-z_]\w*)\s*=[^=]/gm;
|
|
500
|
+
assignRe.lastIndex = 0;
|
|
501
|
+
let am;
|
|
502
|
+
while ((am = assignRe.exec(source)) !== null) {
|
|
503
|
+
const name = am[2];
|
|
504
|
+
const line = (0, lines_1.lineOf)(source, am.index);
|
|
505
|
+
const declLine = lines[line - 1] ?? '';
|
|
506
|
+
// Skip dunder names (__init__, __version__, etc.)
|
|
507
|
+
if (name.startsWith('__'))
|
|
508
|
+
continue;
|
|
509
|
+
// Skip ALL_CAPS constants (conventional module/class-level constants even if indented)
|
|
510
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
511
|
+
continue;
|
|
512
|
+
// Skip self/cls
|
|
513
|
+
if (name === 'self' || name === 'cls')
|
|
514
|
+
continue;
|
|
515
|
+
// Skip if the char before the identifier on this line is a dot (dotted attribute: self.x)
|
|
516
|
+
const beforeName = declLine.slice(0, declLine.indexOf(name));
|
|
517
|
+
if (beforeName.trimEnd().endsWith('.'))
|
|
518
|
+
continue;
|
|
519
|
+
const bodyWithoutDecl = lines.map((l, i) => (i === line - 1 ? '' : l)).join('\n');
|
|
520
|
+
if (!new RegExp(`\\b${name}\\b`).test(bodyWithoutDecl)) {
|
|
521
|
+
unusedVars.push({ name, line });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// --- unused functions ---
|
|
525
|
+
// Only flag private functions (single _ prefix). Public functions may be imported elsewhere.
|
|
526
|
+
for (const fn of functions) {
|
|
527
|
+
if (!fn.name.startsWith('_'))
|
|
528
|
+
continue;
|
|
529
|
+
// Skip dunder methods (__init__, __str__, etc.)
|
|
530
|
+
if (fn.name.startsWith('__'))
|
|
531
|
+
continue;
|
|
532
|
+
const bodyWithoutDecl = lines.map((l, i) => (i === fn.line - 1 ? '' : l)).join('\n');
|
|
533
|
+
if (!new RegExp(`\\b${fn.name}\\b`).test(bodyWithoutDecl)) {
|
|
534
|
+
unusedFunctions.push({ name: fn.name, line: fn.line });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { unusedVars, unusedFunctions };
|
|
538
|
+
}
|
|
539
|
+
// --- warnings ---
|
|
540
|
+
function detectPyWarnings(lineCount, functions, thirdPartyImports, source, config) {
|
|
541
|
+
const warnings = [];
|
|
542
|
+
const sev = (type, fallback) => config.severityOverrides[type] ?? fallback;
|
|
543
|
+
const warnLine = Math.round(config.fileLengthThreshold * 2 / 3);
|
|
544
|
+
if (lineCount > config.fileLengthThreshold) {
|
|
545
|
+
warnings.push({ type: 'file-too-long', message: `File is ${lineCount} lines (>${config.fileLengthThreshold})`, severity: sev('file-too-long', 'error') });
|
|
546
|
+
}
|
|
547
|
+
else if (lineCount > warnLine) {
|
|
548
|
+
warnings.push({ type: 'file-too-long', message: `File is ${lineCount} lines (>${warnLine})`, severity: sev('file-too-long', 'warning') });
|
|
549
|
+
}
|
|
550
|
+
const srcLines = source.split('\n');
|
|
551
|
+
const errorThreshold = config.functionLengthThreshold * 2;
|
|
552
|
+
for (const fn of functions) {
|
|
553
|
+
const endLine = findPythonFunctionEnd(srcLines, fn.line);
|
|
554
|
+
const span = endLine - fn.line + 1;
|
|
555
|
+
if (span > errorThreshold) {
|
|
556
|
+
warnings.push({ type: 'function-too-long', message: `${fn.name} is ~${span} lines (>${errorThreshold})`, severity: sev('function-too-long', 'error'), line: fn.line });
|
|
557
|
+
}
|
|
558
|
+
else if (span > config.functionLengthThreshold) {
|
|
559
|
+
warnings.push({ type: 'function-too-long', message: `${fn.name} is ~${span} lines (>${config.functionLengthThreshold})`, severity: sev('function-too-long', 'warning'), line: fn.line });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (functions.length > config.maxFunctionsPerFile) {
|
|
563
|
+
warnings.push({ type: 'too-many-functions', message: `${functions.length} functions in one file (>${config.maxFunctionsPerFile})`, severity: sev('too-many-functions', 'warning') });
|
|
564
|
+
}
|
|
565
|
+
if (thirdPartyImports.length > config.maxImportsPerFile) {
|
|
566
|
+
warnings.push({ type: 'too-many-imports', message: `${thirdPartyImports.length} third-party imports (>${config.maxImportsPerFile})`, severity: sev('too-many-imports', 'warning') });
|
|
567
|
+
}
|
|
568
|
+
// Public functions: those not starting with _
|
|
569
|
+
const hasPublic = functions.some(f => !f.name.startsWith('_'));
|
|
570
|
+
if (!hasPublic && functions.length > 0) {
|
|
571
|
+
warnings.push({ type: 'no-exports', message: 'No public functions detected', severity: sev('no-exports', 'info') });
|
|
572
|
+
}
|
|
573
|
+
return warnings;
|
|
574
|
+
}
|
|
575
|
+
// --- complexity ---
|
|
576
|
+
function scorePyComplexity(source, codeLines, functions, thirdPartyImports) {
|
|
577
|
+
let score = 1;
|
|
578
|
+
const fnDensity = codeLines > 0 ? functions.length / (codeLines / 50) : 0;
|
|
579
|
+
score += Math.min(3, Math.floor(fnDensity));
|
|
580
|
+
const maxIndent = source.split('\n').reduce((max, line) => {
|
|
581
|
+
const trimmed = line.trimStart();
|
|
582
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
583
|
+
return max;
|
|
584
|
+
return Math.max(max, line.length - trimmed.length);
|
|
585
|
+
}, 0);
|
|
586
|
+
score += Math.min(2, Math.floor(Math.floor(maxIndent / 4) / 3));
|
|
587
|
+
const cfCount = (source.match(/\b(if|elif|else|for|while|try|except|finally|with|match|case|lambda)\b/g) ?? []).length;
|
|
588
|
+
score += Math.min(2, Math.floor(cfCount / 10));
|
|
589
|
+
score += Math.min(2, Math.floor(thirdPartyImports.length / 3));
|
|
590
|
+
return Math.max(1, Math.min(10, score));
|
|
591
|
+
}
|
|
592
|
+
// --- health score ---
|
|
593
|
+
// Shared scorer in ../scoring.ts — Python passes zeroed TypeScript metrics.
|
|
594
|
+
// --- main export ---
|
|
595
|
+
function analysePython(source, config, filePath = '') {
|
|
596
|
+
const rawLines = source.split('\n');
|
|
597
|
+
const lineCount = rawLines.length;
|
|
598
|
+
const blankLines = rawLines.filter(l => l.trim() === '' || l.trim().startsWith('#')).length;
|
|
599
|
+
const codeLines = lineCount - blankLines;
|
|
600
|
+
const allFunctions = detectPyFunctions(source);
|
|
601
|
+
const functions = config.ignoreFunctions.length > 0
|
|
602
|
+
? allFunctions.filter(f => !config.ignoreFunctions.includes(f.name))
|
|
603
|
+
: allFunctions;
|
|
604
|
+
const thirdPartyImports = extractPyThirdPartyImports(source);
|
|
605
|
+
const complexityScore = scorePyComplexity(source, codeLines, functions, thirdPartyImports);
|
|
606
|
+
const warnings = detectPyWarnings(lineCount, functions, thirdPartyImports, source, config);
|
|
607
|
+
const baseSmells = detectPyCodeSmells(source, functions, lineCount, config, (0, guards_1.isTestFile)(filePath));
|
|
608
|
+
const hardcodedSecrets = (0, secrets_1.detectHardcodedSecrets)(source, config, 'python', filePath);
|
|
609
|
+
const secretLines = new Set(hardcodedSecrets.map(s => s.line));
|
|
610
|
+
const codeSmells = {
|
|
611
|
+
...baseSmells,
|
|
612
|
+
unusedVars: baseSmells.unusedVars.filter(v => !secretLines.has(v.line)),
|
|
613
|
+
hardcodedSecrets,
|
|
614
|
+
};
|
|
615
|
+
const healthScore = (0, scoring_1.computeHealthScore)(warnings, ZEROED_TS_METRICS, codeSmells, config);
|
|
616
|
+
return {
|
|
617
|
+
lineCount,
|
|
618
|
+
blankLines,
|
|
619
|
+
codeLines,
|
|
620
|
+
functions,
|
|
621
|
+
thirdPartyImports,
|
|
622
|
+
unusedImports: [], // Python doesn't enforce unused imports at runtime
|
|
623
|
+
complexityScore,
|
|
624
|
+
warnings,
|
|
625
|
+
typeScriptMetrics: ZEROED_TS_METRICS,
|
|
626
|
+
codeSmells,
|
|
627
|
+
healthScore,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
//# sourceMappingURL=analyser.js.map
|