@kernlang/review-python 3.4.6-canary.46.1.19dcfc19 → 3.4.6-canary.53.1.ca3b242b

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.
@@ -0,0 +1,3 @@
1
+ import type { ConceptNode } from '@kernlang/core';
2
+ import type Parser from 'tree-sitter';
3
+ export declare function extractBackgroundTasks(root: Parser.SyntaxNode, source: string, filePath: string, nodes: ConceptNode[]): void;
@@ -0,0 +1,103 @@
1
+ import { conceptId } from '@kernlang/core';
2
+ import { getContainerId, isInAsyncDef, nodeSpan, nodeText, walkNodes } from '../helpers/ast.js';
3
+ // FastAPI BackgroundTasks dispatch.
4
+ //
5
+ // from fastapi import BackgroundTasks
6
+ //
7
+ // @app.post("/email")
8
+ // async def send_email(background_tasks: BackgroundTasks, body: EmailIn):
9
+ // background_tasks.add_task(send_email_func, body.to)
10
+ // ^^^^^^^^^ → effect[background-task]
11
+ //
12
+ // We emit one effect concept per `<param>.add_task(...)` call inside a
13
+ // function whose signature declares a `BackgroundTasks` parameter. The
14
+ // `target` carries the dispatched function name when it can be read from
15
+ // the first positional arg.
16
+ export function extractBackgroundTasks(root, source, filePath, nodes) {
17
+ walkNodes(root, 'function_definition', (fnDef) => {
18
+ const paramNames = collectBackgroundTasksParams(fnDef, source);
19
+ if (paramNames.size === 0)
20
+ return;
21
+ const body = fnDef.childForFieldName('body');
22
+ if (!body)
23
+ return;
24
+ walkNodes(body, 'call', (callNode) => {
25
+ const fnNode = callNode.childForFieldName('function');
26
+ if (!fnNode || fnNode.type !== 'attribute')
27
+ return;
28
+ const obj = fnNode.childForFieldName('object');
29
+ const attr = fnNode.childForFieldName('attribute');
30
+ if (!obj || !attr)
31
+ return;
32
+ if (attr.text !== 'add_task')
33
+ return;
34
+ if (!paramNames.has(obj.text))
35
+ return;
36
+ const target = readTargetName(callNode, source);
37
+ nodes.push({
38
+ id: conceptId(filePath, 'effect', callNode.startIndex),
39
+ kind: 'effect',
40
+ primarySpan: nodeSpan(filePath, callNode),
41
+ evidence: nodeText(source, callNode, 120),
42
+ confidence: 0.95,
43
+ language: 'py',
44
+ containerId: getContainerId(callNode, filePath),
45
+ payload: {
46
+ kind: 'effect',
47
+ subtype: 'background-task',
48
+ async: isInAsyncDef(callNode),
49
+ target,
50
+ },
51
+ });
52
+ });
53
+ });
54
+ }
55
+ function collectBackgroundTasksParams(fnDef, source) {
56
+ const names = new Set();
57
+ const params = fnDef.childForFieldName('parameters');
58
+ if (!params)
59
+ return names;
60
+ for (const child of params.namedChildren) {
61
+ if (child.type !== 'typed_parameter' && child.type !== 'typed_default_parameter')
62
+ continue;
63
+ const typeNode = child.childForFieldName('type');
64
+ if (!typeNode)
65
+ continue;
66
+ const typeText = source.substring(typeNode.startIndex, typeNode.endIndex).trim();
67
+ if (typeText !== 'BackgroundTasks' && typeText !== 'fastapi.BackgroundTasks')
68
+ continue;
69
+ // typed_parameter has the name as its first child identifier;
70
+ // typed_default_parameter exposes it via the `name` field.
71
+ const nameField = child.childForFieldName('name');
72
+ if (nameField) {
73
+ names.add(nameField.text);
74
+ continue;
75
+ }
76
+ const ident = child.namedChildren.find((c) => c.type === 'identifier');
77
+ if (ident)
78
+ names.add(ident.text);
79
+ }
80
+ return names;
81
+ }
82
+ function readTargetName(callNode, source) {
83
+ const args = callNode.childForFieldName('arguments');
84
+ if (!args)
85
+ return undefined;
86
+ // The scheduled callable can be passed positionally or as `func=...`
87
+ // (BackgroundTasks.add_task signature: `add_task(func, *args, **kwargs)`).
88
+ // Take the first positional arg if present; otherwise look for `func=`.
89
+ let funcKeywordValue;
90
+ for (const child of args.namedChildren) {
91
+ if (child.type === 'keyword_argument') {
92
+ const nameNode = child.childForFieldName('name');
93
+ if (nameNode && nameNode.text === 'func') {
94
+ const valueNode = child.childForFieldName('value');
95
+ if (valueNode)
96
+ funcKeywordValue = source.substring(valueNode.startIndex, valueNode.endIndex).trim();
97
+ }
98
+ continue;
99
+ }
100
+ return source.substring(child.startIndex, child.endIndex).trim();
101
+ }
102
+ return funcKeywordValue;
103
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import Parser from 'tree-sitter';
8
8
  import Python from 'tree-sitter-python';
9
+ import { extractBackgroundTasks } from './extractors/background-tasks.js';
9
10
  import { extractDependencyEdges } from './extractors/dependency.js';
10
11
  import { extractEffects } from './extractors/effect.js';
11
12
  import { extractEntrypoints } from './extractors/entrypoint.js';
@@ -28,6 +29,7 @@ export function extractPythonConcepts(source, filePath) {
28
29
  extractErrorRaise(tree.rootNode, source, filePath, nodes);
29
30
  extractErrorHandle(tree.rootNode, source, filePath, nodes);
30
31
  extractEffects(tree.rootNode, source, filePath, nodes);
32
+ extractBackgroundTasks(tree.rootNode, source, filePath, nodes);
31
33
  extractEntrypoints(tree.rootNode, source, filePath, nodes);
32
34
  extractGuards(tree.rootNode, source, filePath, nodes);
33
35
  extractStateMutation(tree.rootNode, source, filePath, nodes);
@@ -3,7 +3,6 @@ export declare const NETWORK_MODULES: Set<string>;
3
3
  export declare const NETWORK_METHODS: Set<string>;
4
4
  export declare const DB_MODULES: Set<string>;
5
5
  export declare const DB_METHODS: Set<string>;
6
- export declare const _FS_FUNCTIONS: Set<string>;
7
6
  export declare const PY_API_ERROR_STATUS_CODES: Set<number>;
8
7
  export declare const PY_API_SUCCESS_STATUS_CODES: Set<number>;
9
8
  export declare const FASTAPI_DEFAULT_SUCCESS_STATUS = 200;
@@ -26,7 +26,6 @@ export const DB_METHODS = new Set([
26
26
  'update_one',
27
27
  'delete_one',
28
28
  ]);
29
- export const _FS_FUNCTIONS = new Set(['open', 'read', 'write', 'readlines', 'writelines']);
30
29
  export const PY_API_ERROR_STATUS_CODES = new Set([401, 403, 404, 422, 500]);
31
30
  export const PY_API_SUCCESS_STATUS_CODES = new Set([200, 201, 202, 204, 206]);
32
31
  // FastAPI's documented default success status is 200, regardless of HTTP method
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernlang/review-python",
3
- "version": "3.4.6-canary.46.1.19dcfc19",
3
+ "version": "3.4.6-canary.53.1.ca3b242b",
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": "3.4.6-canary.46.1.19dcfc19",
12
- "@kernlang/review": "3.4.6-canary.46.1.19dcfc19"
11
+ "@kernlang/core": "3.4.6-canary.53.1.ca3b242b",
12
+ "@kernlang/review": "3.4.6-canary.53.1.ca3b242b"
13
13
  },
14
14
  "devDependencies": {
15
15
  "ts-morph": "^28.0.0",
@@ -0,0 +1,107 @@
1
+ import type { ConceptNode } from '@kernlang/core';
2
+ import { conceptId } from '@kernlang/core';
3
+ import type Parser from 'tree-sitter';
4
+ import { getContainerId, isInAsyncDef, nodeSpan, nodeText, walkNodes } from '../helpers/ast.js';
5
+
6
+ // FastAPI BackgroundTasks dispatch.
7
+ //
8
+ // from fastapi import BackgroundTasks
9
+ //
10
+ // @app.post("/email")
11
+ // async def send_email(background_tasks: BackgroundTasks, body: EmailIn):
12
+ // background_tasks.add_task(send_email_func, body.to)
13
+ // ^^^^^^^^^ → effect[background-task]
14
+ //
15
+ // We emit one effect concept per `<param>.add_task(...)` call inside a
16
+ // function whose signature declares a `BackgroundTasks` parameter. The
17
+ // `target` carries the dispatched function name when it can be read from
18
+ // the first positional arg.
19
+ export function extractBackgroundTasks(
20
+ root: Parser.SyntaxNode,
21
+ source: string,
22
+ filePath: string,
23
+ nodes: ConceptNode[],
24
+ ): void {
25
+ walkNodes(root, 'function_definition', (fnDef) => {
26
+ const paramNames = collectBackgroundTasksParams(fnDef, source);
27
+ if (paramNames.size === 0) return;
28
+
29
+ const body = fnDef.childForFieldName('body');
30
+ if (!body) return;
31
+
32
+ walkNodes(body, 'call', (callNode) => {
33
+ const fnNode = callNode.childForFieldName('function');
34
+ if (!fnNode || fnNode.type !== 'attribute') return;
35
+
36
+ const obj = fnNode.childForFieldName('object');
37
+ const attr = fnNode.childForFieldName('attribute');
38
+ if (!obj || !attr) return;
39
+ if (attr.text !== 'add_task') return;
40
+ if (!paramNames.has(obj.text)) return;
41
+
42
+ const target = readTargetName(callNode, source);
43
+ nodes.push({
44
+ id: conceptId(filePath, 'effect', callNode.startIndex),
45
+ kind: 'effect',
46
+ primarySpan: nodeSpan(filePath, callNode),
47
+ evidence: nodeText(source, callNode, 120),
48
+ confidence: 0.95,
49
+ language: 'py',
50
+ containerId: getContainerId(callNode, filePath),
51
+ payload: {
52
+ kind: 'effect',
53
+ subtype: 'background-task',
54
+ async: isInAsyncDef(callNode),
55
+ target,
56
+ },
57
+ });
58
+ });
59
+ });
60
+ }
61
+
62
+ function collectBackgroundTasksParams(fnDef: Parser.SyntaxNode, source: string): Set<string> {
63
+ const names = new Set<string>();
64
+ const params = fnDef.childForFieldName('parameters');
65
+ if (!params) return names;
66
+
67
+ for (const child of params.namedChildren) {
68
+ if (child.type !== 'typed_parameter' && child.type !== 'typed_default_parameter') continue;
69
+ const typeNode = child.childForFieldName('type');
70
+ if (!typeNode) continue;
71
+ const typeText = source.substring(typeNode.startIndex, typeNode.endIndex).trim();
72
+ if (typeText !== 'BackgroundTasks' && typeText !== 'fastapi.BackgroundTasks') continue;
73
+
74
+ // typed_parameter has the name as its first child identifier;
75
+ // typed_default_parameter exposes it via the `name` field.
76
+ const nameField = child.childForFieldName('name');
77
+ if (nameField) {
78
+ names.add(nameField.text);
79
+ continue;
80
+ }
81
+ const ident = child.namedChildren.find((c) => c.type === 'identifier');
82
+ if (ident) names.add(ident.text);
83
+ }
84
+ return names;
85
+ }
86
+
87
+ function readTargetName(callNode: Parser.SyntaxNode, source: string): string | undefined {
88
+ const args = callNode.childForFieldName('arguments');
89
+ if (!args) return undefined;
90
+
91
+ // The scheduled callable can be passed positionally or as `func=...`
92
+ // (BackgroundTasks.add_task signature: `add_task(func, *args, **kwargs)`).
93
+ // Take the first positional arg if present; otherwise look for `func=`.
94
+ let funcKeywordValue: string | undefined;
95
+ for (const child of args.namedChildren) {
96
+ if (child.type === 'keyword_argument') {
97
+ const nameNode = child.childForFieldName('name');
98
+ if (nameNode && nameNode.text === 'func') {
99
+ const valueNode = child.childForFieldName('value');
100
+ if (valueNode) funcKeywordValue = source.substring(valueNode.startIndex, valueNode.endIndex).trim();
101
+ }
102
+ continue;
103
+ }
104
+ return source.substring(child.startIndex, child.endIndex).trim();
105
+ }
106
+ return funcKeywordValue;
107
+ }
@@ -8,6 +8,7 @@
8
8
  import type { ConceptEdge, ConceptMap, ConceptNode } from '@kernlang/core';
9
9
  import Parser from 'tree-sitter';
10
10
  import Python from 'tree-sitter-python';
11
+ import { extractBackgroundTasks } from './extractors/background-tasks.js';
11
12
  import { extractDependencyEdges } from './extractors/dependency.js';
12
13
  import { extractEffects } from './extractors/effect.js';
13
14
  import { extractEntrypoints } from './extractors/entrypoint.js';
@@ -34,6 +35,7 @@ export function extractPythonConcepts(source: string, filePath: string): Concept
34
35
  extractErrorRaise(tree.rootNode, source, filePath, nodes);
35
36
  extractErrorHandle(tree.rootNode, source, filePath, nodes);
36
37
  extractEffects(tree.rootNode, source, filePath, nodes);
38
+ extractBackgroundTasks(tree.rootNode, source, filePath, nodes);
37
39
 
38
40
  extractEntrypoints(tree.rootNode, source, filePath, nodes);
39
41
  extractGuards(tree.rootNode, source, filePath, nodes);
@@ -29,8 +29,6 @@ export const DB_METHODS = new Set([
29
29
  'delete_one',
30
30
  ]);
31
31
 
32
- export const _FS_FUNCTIONS = new Set(['open', 'read', 'write', 'readlines', 'writelines']);
33
-
34
32
  export const PY_API_ERROR_STATUS_CODES = new Set([401, 403, 404, 422, 500]);
35
33
  export const PY_API_SUCCESS_STATUS_CODES = new Set([200, 201, 202, 204, 206]);
36
34
  // FastAPI's documented default success status is 200, regardless of HTTP method
@@ -0,0 +1,87 @@
1
+ /// <reference types="jest" />
2
+ import type { ConceptNode, EffectPayload } from '@kernlang/core';
3
+ import { extractPythonConcepts } from '../src/mapper.js';
4
+
5
+ function isEffectNode(node: ConceptNode): node is ConceptNode & { payload: EffectPayload } {
6
+ return node.kind === 'effect' && node.payload.kind === 'effect';
7
+ }
8
+
9
+ function backgroundTasks(source: string) {
10
+ return extractPythonConcepts(source, 'app/api/email.py')
11
+ .nodes.filter(isEffectNode)
12
+ .filter((node) => node.payload.subtype === 'background-task');
13
+ }
14
+
15
+ describe('FastAPI BackgroundTasks extraction', () => {
16
+ it('emits one background-task effect per add_task call', () => {
17
+ const effects = backgroundTasks(`
18
+ from fastapi import BackgroundTasks
19
+
20
+ @app.post("/email")
21
+ async def send_email(background_tasks: BackgroundTasks, body: dict):
22
+ background_tasks.add_task(send_email_func, body["to"])
23
+ background_tasks.add_task(log_email, body["to"])
24
+ return {"ok": True}
25
+ `);
26
+
27
+ expect(effects).toHaveLength(2);
28
+ expect(effects[0].payload.subtype).toBe('background-task');
29
+ expect(effects[0].payload.target).toBe('send_email_func');
30
+ expect(effects[0].payload.async).toBe(true);
31
+ expect(effects[1].payload.target).toBe('log_email');
32
+ });
33
+
34
+ it('only fires when the param is typed BackgroundTasks', () => {
35
+ const effects = backgroundTasks(`
36
+ @app.post("/email")
37
+ async def send_email(other: SomeOther):
38
+ other.add_task(send_email_func)
39
+ return {"ok": True}
40
+ `);
41
+
42
+ expect(effects).toHaveLength(0);
43
+ });
44
+
45
+ it('matches against the param name, not arbitrary identifiers', () => {
46
+ const effects = backgroundTasks(`
47
+ from fastapi import BackgroundTasks
48
+
49
+ @app.post("/email")
50
+ async def send_email(bg: BackgroundTasks):
51
+ bg.add_task(send_email_func)
52
+ other.add_task(should_not_match)
53
+ return {"ok": True}
54
+ `);
55
+
56
+ expect(effects).toHaveLength(1);
57
+ expect(effects[0].payload.target).toBe('send_email_func');
58
+ });
59
+
60
+ it('handles add_task(func=...) keyword form', () => {
61
+ const effects = backgroundTasks(`
62
+ from fastapi import BackgroundTasks
63
+
64
+ @app.post("/email")
65
+ async def send_email(background_tasks: BackgroundTasks):
66
+ background_tasks.add_task(func=send_email_func, to="x@y")
67
+ return {"ok": True}
68
+ `);
69
+
70
+ expect(effects).toHaveLength(1);
71
+ expect(effects[0].payload.target).toBe('send_email_func');
72
+ });
73
+
74
+ it('records async=false for sync route handlers', () => {
75
+ const effects = backgroundTasks(`
76
+ from fastapi import BackgroundTasks
77
+
78
+ @app.post("/email")
79
+ def send_email(background_tasks: BackgroundTasks):
80
+ background_tasks.add_task(send_email_func)
81
+ return {"ok": True}
82
+ `);
83
+
84
+ expect(effects).toHaveLength(1);
85
+ expect(effects[0].payload.async).toBe(false);
86
+ });
87
+ });
@@ -1 +1 @@
1
- {"root":["./src/index.ts","./src/mapper.ts","./src/mapper/index.ts","./src/mapper/signatures.ts","./src/mapper/extractors/dependency.ts","./src/mapper/extractors/effect.ts","./src/mapper/extractors/entrypoint.ts","./src/mapper/extractors/error.ts","./src/mapper/extractors/fastapi-pagination.ts","./src/mapper/extractors/fastapi-status.ts","./src/mapper/extractors/guard.ts","./src/mapper/extractors/pydantic.ts","./src/mapper/extractors/state-mutation.ts","./src/mapper/helpers/ast.ts","./src/mapper/helpers/types.ts"],"version":"6.0.3"}
1
+ {"root":["./src/index.ts","./src/mapper.ts","./src/mapper/index.ts","./src/mapper/signatures.ts","./src/mapper/extractors/background-tasks.ts","./src/mapper/extractors/dependency.ts","./src/mapper/extractors/effect.ts","./src/mapper/extractors/entrypoint.ts","./src/mapper/extractors/error.ts","./src/mapper/extractors/fastapi-pagination.ts","./src/mapper/extractors/fastapi-status.ts","./src/mapper/extractors/guard.ts","./src/mapper/extractors/pydantic.ts","./src/mapper/extractors/state-mutation.ts","./src/mapper/helpers/ast.ts","./src/mapper/helpers/types.ts"],"version":"6.0.3"}