@kernlang/review-python 4.1.1-canary.227.1.dcaa3f78 → 4.1.1-canary.232.1.c7f7511c

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.
@@ -23,7 +23,7 @@ export function extractBackgroundTasks(root, source, filePath, nodes) {
23
23
  return;
24
24
  walkNodes(body, 'call', (callNode) => {
25
25
  const fnNode = callNode.childForFieldName('function');
26
- if (!fnNode || fnNode.type !== 'attribute')
26
+ if (fnNode?.type !== 'attribute')
27
27
  return;
28
28
  const obj = fnNode.childForFieldName('object');
29
29
  const attr = fnNode.childForFieldName('attribute');
@@ -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
- if (isIntentionalNoopExcept(exceptNode, source))
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
- if (isIntentionalNoopExcept(exceptNode, source))
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
- if (isIntentionalNoopExcept(exceptNode, source))
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;
@@ -54,10 +54,10 @@ export function extractGuards(root, source, filePath, nodes) {
54
54
  // Feeds the `auth-drift` cross-stack rule.
55
55
  walkNodes(root, 'default_parameter', (node) => {
56
56
  const val = node.childForFieldName('value');
57
- if (!val || val.type !== 'call')
57
+ if (val?.type !== 'call')
58
58
  return;
59
59
  const func = val.childForFieldName('function');
60
- if (!func || func.text !== 'Depends')
60
+ if (func?.text !== 'Depends')
61
61
  return;
62
62
  const args = val.childForFieldName('arguments');
63
63
  if (!args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernlang/review-python",
3
- "version": "4.1.1-canary.227.1.dcaa3f78",
3
+ "version": "4.1.1-canary.232.1.c7f7511c",
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/review": "4.1.1-canary.227.1.dcaa3f78",
12
- "@kernlang/core": "4.1.1-canary.227.1.dcaa3f78"
11
+ "@kernlang/core": "4.1.1-canary.232.1.c7f7511c",
12
+ "@kernlang/review": "4.1.1-canary.232.1.c7f7511c"
13
13
  },
14
14
  "devDependencies": {
15
15
  "ts-morph": "^28.0.0",
@@ -31,7 +31,7 @@ export function extractBackgroundTasks(
31
31
 
32
32
  walkNodes(body, 'call', (callNode) => {
33
33
  const fnNode = callNode.childForFieldName('function');
34
- if (!fnNode || fnNode.type !== 'attribute') return;
34
+ if (fnNode?.type !== 'attribute') return;
35
35
 
36
36
  const obj = fnNode.childForFieldName('object');
37
37
  const attr = fnNode.childForFieldName('attribute');
@@ -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
- if (isIntentionalNoopExcept(exceptNode, source)) return { type: 'wrapped', confidence: 0.55 };
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
- if (isIntentionalNoopExcept(exceptNode, source)) return { type: 'wrapped', confidence: 0.55 };
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
- if (isIntentionalNoopExcept(exceptNode, source)) return { type: 'wrapped', confidence: 0.55 };
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;
@@ -58,9 +58,9 @@ export function extractGuards(root: Parser.SyntaxNode, source: string, filePath:
58
58
  // Feeds the `auth-drift` cross-stack rule.
59
59
  walkNodes(root, 'default_parameter', (node) => {
60
60
  const val = node.childForFieldName('value');
61
- if (!val || val.type !== 'call') return;
61
+ if (val?.type !== 'call') return;
62
62
  const func = val.childForFieldName('function');
63
- if (!func || func.text !== 'Depends') return;
63
+ if (func?.text !== 'Depends') return;
64
64
  const args = val.childForFieldName('arguments');
65
65
  if (!args) return;
66
66
  const posArg = args.namedChildren.find((c) => c.type === 'identifier' || c.type === 'attribute');
@@ -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
+ });