@kernlang/review-python 4.1.1-canary.228.1.41c150f2 → 4.5.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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { conceptId } from '@kernlang/core';
|
|
2
|
+
import { PYTHON_BUILTIN_EXCEPTIONS } from '@kernlang/review';
|
|
2
3
|
import { getContainerId, nodeSpan, nodeText, walkNodes } from '../helpers/ast.js';
|
|
3
4
|
import { PY_API_ERROR_STATUS_CODES } from '../signatures.js';
|
|
4
5
|
export function extractErrorRaise(root, source, filePath, nodes) {
|
|
@@ -49,24 +50,18 @@ function classifyPythonDisposition(exceptNode, block, source) {
|
|
|
49
50
|
const children = block.namedChildren;
|
|
50
51
|
// except: pass → ignored
|
|
51
52
|
if (children.length === 1 && children[0].type === 'pass_statement') {
|
|
52
|
-
|
|
53
|
-
return { type: 'wrapped', confidence: 0.55 };
|
|
54
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
53
|
+
return noopDisposition(exceptNode, block, source);
|
|
55
54
|
}
|
|
56
55
|
// except: ... (ellipsis) → ignored
|
|
57
56
|
if (children.length === 1 && children[0].type === 'expression_statement') {
|
|
58
57
|
const text = source.substring(children[0].startIndex, children[0].endIndex).trim();
|
|
59
58
|
if (text === '...') {
|
|
60
|
-
|
|
61
|
-
return { type: 'wrapped', confidence: 0.55 };
|
|
62
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
59
|
+
return noopDisposition(exceptNode, block, source);
|
|
63
60
|
}
|
|
64
61
|
}
|
|
65
62
|
// Empty block
|
|
66
63
|
if (children.length === 0) {
|
|
67
|
-
|
|
68
|
-
return { type: 'wrapped', confidence: 0.55 };
|
|
69
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
64
|
+
return noopDisposition(exceptNode, block, source);
|
|
70
65
|
}
|
|
71
66
|
const bodyText = source.substring(block.startIndex, block.endIndex);
|
|
72
67
|
// raise → rethrown or wrapped
|
|
@@ -89,6 +84,39 @@ function classifyPythonDisposition(exceptNode, block, source) {
|
|
|
89
84
|
}
|
|
90
85
|
return { type: 'wrapped', confidence: 0.5 };
|
|
91
86
|
}
|
|
87
|
+
// A silent swallow (`pass` / `...` / empty block) reads as a genuine "ignored"
|
|
88
|
+
// error ONLY when the catch is broad/bare AND undocumented. Two shapes are
|
|
89
|
+
// expected patterns, not oversights, and downgrade to 'wrapped':
|
|
90
|
+
// • a narrow, NON-builtin (domain/library) exception — `except IntegrityError:
|
|
91
|
+
// pass` (dedupe on a unique constraint), `except ProgressDataNotFoundException:
|
|
92
|
+
// pass` (optional section). Builtin exceptions stay flaggable because they
|
|
93
|
+
// are broad enough to hide an UNRELATED failure (`except OSError: pass`
|
|
94
|
+
// around mixed I/O).
|
|
95
|
+
// • an explanatory comment in the except header or block — a conscious
|
|
96
|
+
// decision ("# instrumentation must never break a chat turn").
|
|
97
|
+
// This closed 4 correct-swallow false positives kern-guard raised on fitvt PR #16.
|
|
98
|
+
function noopDisposition(exceptNode, block, source) {
|
|
99
|
+
const intentional = isIntentionalNoopExcept(exceptNode, source) ||
|
|
100
|
+
isNarrowNonBuiltinExcept(exceptNode, block, source) ||
|
|
101
|
+
hasExplanatoryComment(exceptNode, block, source);
|
|
102
|
+
return intentional ? { type: 'wrapped', confidence: 0.55 } : { type: 'ignored', confidence: 1.0 };
|
|
103
|
+
}
|
|
104
|
+
// True when EVERY caught type is a narrow, non-builtin exception. Bare `except:`
|
|
105
|
+
// (no types) and any builtin / `Exception` / `BaseException` disqualify it, so a
|
|
106
|
+
// mixed `except (IntegrityError, Exception):` stays flaggable.
|
|
107
|
+
function isNarrowNonBuiltinExcept(exceptNode, block, source) {
|
|
108
|
+
const types = parseExceptTypes(source.substring(exceptNode.startIndex, block.startIndex));
|
|
109
|
+
if (types.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
return types.every((t) => !PYTHON_BUILTIN_EXCEPTIONS.has(t));
|
|
112
|
+
}
|
|
113
|
+
// True when the except header or its (pass/…/empty) block carries a `#` comment.
|
|
114
|
+
// The block body here is only pass/…/empty, and an except header holds no string
|
|
115
|
+
// literals, so any `#` in this contiguous source range is a comment — the scan
|
|
116
|
+
// is safe without string-stripping.
|
|
117
|
+
function hasExplanatoryComment(exceptNode, block, source) {
|
|
118
|
+
return source.substring(exceptNode.startIndex, block.endIndex).includes('#');
|
|
119
|
+
}
|
|
92
120
|
function isIntentionalNoopExcept(exceptNode, source) {
|
|
93
121
|
const block = exceptNode.children.find((child) => child.type === 'block');
|
|
94
122
|
const headerEnd = block?.startIndex ?? exceptNode.endIndex;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernlang/review-python",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Python concept mapper for kern review — tree-sitter based",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"tree-sitter": "^0.25.0",
|
|
10
10
|
"tree-sitter-python": "^0.25.0",
|
|
11
|
-
"@kernlang/core": "4.
|
|
12
|
-
"@kernlang/review": "4.
|
|
11
|
+
"@kernlang/core": "4.5.0",
|
|
12
|
+
"@kernlang/review": "4.5.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"ts-morph": "^28.0.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConceptNode, ErrorHandlePayload } from '@kernlang/core';
|
|
2
2
|
import { conceptId } from '@kernlang/core';
|
|
3
|
+
import { PYTHON_BUILTIN_EXCEPTIONS } from '@kernlang/review';
|
|
3
4
|
import type Parser from 'tree-sitter';
|
|
4
5
|
import { getContainerId, nodeSpan, nodeText, walkNodes } from '../helpers/ast.js';
|
|
5
6
|
import { PY_API_ERROR_STATUS_CODES } from '../signatures.js';
|
|
@@ -70,23 +71,20 @@ function classifyPythonDisposition(
|
|
|
70
71
|
|
|
71
72
|
// except: pass → ignored
|
|
72
73
|
if (children.length === 1 && children[0].type === 'pass_statement') {
|
|
73
|
-
|
|
74
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
74
|
+
return noopDisposition(exceptNode, block, source);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// except: ... (ellipsis) → ignored
|
|
78
78
|
if (children.length === 1 && children[0].type === 'expression_statement') {
|
|
79
79
|
const text = source.substring(children[0].startIndex, children[0].endIndex).trim();
|
|
80
80
|
if (text === '...') {
|
|
81
|
-
|
|
82
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
81
|
+
return noopDisposition(exceptNode, block, source);
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
// Empty block
|
|
87
86
|
if (children.length === 0) {
|
|
88
|
-
|
|
89
|
-
return { type: 'ignored', confidence: 1.0 };
|
|
87
|
+
return noopDisposition(exceptNode, block, source);
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
const bodyText = source.substring(block.startIndex, block.endIndex);
|
|
@@ -114,6 +112,46 @@ function classifyPythonDisposition(
|
|
|
114
112
|
return { type: 'wrapped', confidence: 0.5 };
|
|
115
113
|
}
|
|
116
114
|
|
|
115
|
+
// A silent swallow (`pass` / `...` / empty block) reads as a genuine "ignored"
|
|
116
|
+
// error ONLY when the catch is broad/bare AND undocumented. Two shapes are
|
|
117
|
+
// expected patterns, not oversights, and downgrade to 'wrapped':
|
|
118
|
+
// • a narrow, NON-builtin (domain/library) exception — `except IntegrityError:
|
|
119
|
+
// pass` (dedupe on a unique constraint), `except ProgressDataNotFoundException:
|
|
120
|
+
// pass` (optional section). Builtin exceptions stay flaggable because they
|
|
121
|
+
// are broad enough to hide an UNRELATED failure (`except OSError: pass`
|
|
122
|
+
// around mixed I/O).
|
|
123
|
+
// • an explanatory comment in the except header or block — a conscious
|
|
124
|
+
// decision ("# instrumentation must never break a chat turn").
|
|
125
|
+
// This closed 4 correct-swallow false positives kern-guard raised on fitvt PR #16.
|
|
126
|
+
function noopDisposition(
|
|
127
|
+
exceptNode: Parser.SyntaxNode,
|
|
128
|
+
block: Parser.SyntaxNode,
|
|
129
|
+
source: string,
|
|
130
|
+
): { type: ErrorHandlePayload['disposition']; confidence: number } {
|
|
131
|
+
const intentional =
|
|
132
|
+
isIntentionalNoopExcept(exceptNode, source) ||
|
|
133
|
+
isNarrowNonBuiltinExcept(exceptNode, block, source) ||
|
|
134
|
+
hasExplanatoryComment(exceptNode, block, source);
|
|
135
|
+
return intentional ? { type: 'wrapped', confidence: 0.55 } : { type: 'ignored', confidence: 1.0 };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// True when EVERY caught type is a narrow, non-builtin exception. Bare `except:`
|
|
139
|
+
// (no types) and any builtin / `Exception` / `BaseException` disqualify it, so a
|
|
140
|
+
// mixed `except (IntegrityError, Exception):` stays flaggable.
|
|
141
|
+
function isNarrowNonBuiltinExcept(exceptNode: Parser.SyntaxNode, block: Parser.SyntaxNode, source: string): boolean {
|
|
142
|
+
const types = parseExceptTypes(source.substring(exceptNode.startIndex, block.startIndex));
|
|
143
|
+
if (types.length === 0) return false;
|
|
144
|
+
return types.every((t) => !PYTHON_BUILTIN_EXCEPTIONS.has(t));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// True when the except header or its (pass/…/empty) block carries a `#` comment.
|
|
148
|
+
// The block body here is only pass/…/empty, and an except header holds no string
|
|
149
|
+
// literals, so any `#` in this contiguous source range is a comment — the scan
|
|
150
|
+
// is safe without string-stripping.
|
|
151
|
+
function hasExplanatoryComment(exceptNode: Parser.SyntaxNode, block: Parser.SyntaxNode, source: string): boolean {
|
|
152
|
+
return source.substring(exceptNode.startIndex, block.endIndex).includes('#');
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
function isIntentionalNoopExcept(exceptNode: Parser.SyntaxNode, source: string): boolean {
|
|
118
156
|
const block = exceptNode.children.find((child) => child.type === 'block');
|
|
119
157
|
const headerEnd = block?.startIndex ?? exceptNode.endIndex;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Precision tests for the Python ignored-error classifier.
|
|
3
|
+
*
|
|
4
|
+
* kern-guard on fitvt PR #16 flagged four CORRECT swallows as ignored-error
|
|
5
|
+
* false positives. A silent swallow (`pass`/`...`/empty) should read as
|
|
6
|
+
* "ignored" ONLY when the catch is broad/bare AND undocumented; a narrow
|
|
7
|
+
* non-builtin (domain/library) exception, or any explanatory comment, is an
|
|
8
|
+
* intentional decision. Builtin exceptions stay flaggable because they are
|
|
9
|
+
* broad enough to hide an unrelated failure.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { runConceptRules } from '@kernlang/review';
|
|
13
|
+
import { extractPythonConcepts } from '../src/mapper.js';
|
|
14
|
+
|
|
15
|
+
function fires(source: string): boolean {
|
|
16
|
+
const concepts = extractPythonConcepts(source, 'test.py');
|
|
17
|
+
const findings = runConceptRules(concepts, 'test.py');
|
|
18
|
+
return Boolean(findings.find((f) => f.ruleId === 'ignored-error'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('Python ignored-error precision (fitvt PR #16)', () => {
|
|
22
|
+
it('does NOT fire on a narrow non-builtin exception swallow (dedupe pattern)', () => {
|
|
23
|
+
expect(
|
|
24
|
+
fires(`
|
|
25
|
+
try:
|
|
26
|
+
async with db.begin_nested():
|
|
27
|
+
db.add(CustomFoodReport(custom_food_id=cid))
|
|
28
|
+
except IntegrityError:
|
|
29
|
+
pass
|
|
30
|
+
`),
|
|
31
|
+
).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does NOT fire on a narrow domain-exception swallow (optional section)', () => {
|
|
35
|
+
expect(
|
|
36
|
+
fires(`
|
|
37
|
+
try:
|
|
38
|
+
weight = get_weight_progress(user_id)
|
|
39
|
+
summary["weight"] = weight
|
|
40
|
+
except ProgressDataNotFoundException:
|
|
41
|
+
pass
|
|
42
|
+
`),
|
|
43
|
+
).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does NOT fire on a broad catch with an inline explanatory comment', () => {
|
|
47
|
+
expect(
|
|
48
|
+
fires(`
|
|
49
|
+
try:
|
|
50
|
+
logger.info("metric %d", n)
|
|
51
|
+
except Exception: # instrumentation must never break a chat turn
|
|
52
|
+
pass
|
|
53
|
+
`),
|
|
54
|
+
).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('does NOT fire on a broad catch with a standalone explanatory comment', () => {
|
|
58
|
+
expect(
|
|
59
|
+
fires(`
|
|
60
|
+
try:
|
|
61
|
+
return verify_token(header).get("sub")
|
|
62
|
+
except Exception:
|
|
63
|
+
# Never let token-parsing errors block or crash the limiter.
|
|
64
|
+
pass
|
|
65
|
+
`),
|
|
66
|
+
).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('STILL fires on a broad, undocumented Exception swallow', () => {
|
|
70
|
+
expect(
|
|
71
|
+
fires(`
|
|
72
|
+
try:
|
|
73
|
+
analytics["body_fat"] = get_average_body_fat(user_id)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
`),
|
|
77
|
+
).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('STILL fires on a bare except: pass', () => {
|
|
81
|
+
expect(
|
|
82
|
+
fires(`
|
|
83
|
+
try:
|
|
84
|
+
do_work()
|
|
85
|
+
except:
|
|
86
|
+
pass
|
|
87
|
+
`),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('STILL fires on a narrow BUILTIN exception mixed with real work (can hide unrelated failure)', () => {
|
|
92
|
+
expect(
|
|
93
|
+
fires(`
|
|
94
|
+
try:
|
|
95
|
+
os.close(fd)
|
|
96
|
+
open(path).read()
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
`),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('STILL fires when a non-builtin is mixed with a broad Exception', () => {
|
|
104
|
+
expect(
|
|
105
|
+
fires(`
|
|
106
|
+
try:
|
|
107
|
+
db.add(row)
|
|
108
|
+
except (IntegrityError, Exception):
|
|
109
|
+
pass
|
|
110
|
+
`),
|
|
111
|
+
).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('STILL fires on the 3.11 broad builtins ExceptionGroup / BaseExceptionGroup', () => {
|
|
115
|
+
expect(fires(`\ntry:\n do_work()\nexcept ExceptionGroup:\n pass\n`)).toBe(true);
|
|
116
|
+
expect(fires(`\ntry:\n do_work()\nexcept BaseExceptionGroup:\n pass\n`)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('STILL fires on builtin Warning subclasses (e.g. DeprecationWarning)', () => {
|
|
120
|
+
expect(fires(`\ntry:\n do_work()\nexcept DeprecationWarning:\n pass\n`)).toBe(true);
|
|
121
|
+
expect(fires(`\ntry:\n do_work()\nexcept ResourceWarning:\n pass\n`)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does NOT fire on a narrow non-builtin in a MULTI-LINE tuple header', () => {
|
|
125
|
+
// The tree-sitter header span covers all lines; the fallback joins them.
|
|
126
|
+
expect(
|
|
127
|
+
fires(`
|
|
128
|
+
try:
|
|
129
|
+
db.add(row)
|
|
130
|
+
except (
|
|
131
|
+
IntegrityError,
|
|
132
|
+
):
|
|
133
|
+
pass
|
|
134
|
+
`),
|
|
135
|
+
).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|