-
Notifications
You must be signed in to change notification settings - Fork 14
fix(native): resolve Go factory and Python constructor receiver types #1498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3892e7d
ef8ea4f
a372b82
9a52c7c
85a26df
184d221
909e1df
66fc899
84e1a5f
d07b358
3db5d8c
8b3aa3d
fd4ffd1
498ee21
61a9839
5f5d4d2
29dd101
9320ed2
7313330
f9608be
d6c4d3c
90df67b
10bc11f
6ea7b3f
d6c13b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -317,6 +317,53 @@ fn extract_python_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Optio | |
| } | ||
| } | ||
|
|
||
| /// Python builtins / stdlib classes that start with an uppercase letter and would | ||
| /// false-positive on the constructor-call heuristic. Mirrors `BUILTIN_GLOBALS_PY` | ||
| /// in `src/extractors/python.ts`. | ||
| fn is_python_builtin(name: &str) -> bool { | ||
| matches!( | ||
| name, | ||
| "Exception" | ||
| | "BaseException" | ||
| | "ValueError" | ||
| | "TypeError" | ||
| | "KeyError" | ||
| | "IndexError" | ||
| | "AttributeError" | ||
| | "RuntimeError" | ||
| | "OSError" | ||
| | "IOError" | ||
| | "FileNotFoundError" | ||
| | "PermissionError" | ||
| | "NotImplementedError" | ||
| | "StopIteration" | ||
| | "GeneratorExit" | ||
| | "SystemExit" | ||
| | "KeyboardInterrupt" | ||
| | "ArithmeticError" | ||
| | "LookupError" | ||
| | "UnicodeError" | ||
| | "UnicodeDecodeError" | ||
| | "UnicodeEncodeError" | ||
| | "ImportError" | ||
| | "ModuleNotFoundError" | ||
| | "ConnectionError" | ||
| | "TimeoutError" | ||
| | "OverflowError" | ||
| | "ZeroDivisionError" | ||
| | "NameError" | ||
| | "SyntaxError" | ||
| | "RecursionError" | ||
| | "MemoryError" | ||
| | "Path" | ||
| | "PurePath" | ||
| | "OrderedDict" | ||
| | "Counter" | ||
| | "Decimal" | ||
| | "Fraction" | ||
| ) | ||
| } | ||
|
|
||
| fn match_python_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) { | ||
| match node.kind() { | ||
| "typed_parameter" => { | ||
|
|
@@ -357,6 +404,52 @@ fn match_python_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, | |
| } | ||
| } | ||
| } | ||
| // `order = Order(...)` → seed order : Order at conf 1.0. | ||
| // `obj = module.Class(...)` → seed obj : module at conf 0.7 (factory pattern). | ||
| // Mirrors `handlePyAssignmentType` in `src/extractors/python.ts`. | ||
| "assignment" => { | ||
| infer_py_assignment_type(node, source, &mut symbols.type_map); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| /// Seed typeMap from plain Python assignments where the RHS is a constructor or factory call. | ||
| fn infer_py_assignment_type(node: &Node, source: &[u8], type_map: &mut Vec<TypeMapEntry>) { | ||
| let Some(left) = node.child_by_field_name("left") else { return }; | ||
| let Some(right) = node.child_by_field_name("right") else { return }; | ||
| if left.kind() != "identifier" || right.kind() != "call" { return; } | ||
| let var_name = node_text(&left, source).to_string(); | ||
| let Some(fn_node) = right.child_by_field_name("function") else { return }; | ||
| match fn_node.kind() { | ||
| "identifier" => { | ||
| // `order = Order(...)` — uppercase first char → constructor, conf 1.0. | ||
| let name = node_text(&fn_node, source); | ||
| if name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) { | ||
| type_map.push(TypeMapEntry { | ||
| name: var_name, | ||
| type_name: name.to_string(), | ||
| confidence: 1.0, | ||
| }); | ||
| } | ||
| } | ||
| "attribute" => { | ||
| // `obj = Module.Class(...)` — uppercase object name, not a builtin → conf 0.7. | ||
| if let Some(obj_node) = fn_node.child_by_field_name("object") { | ||
| if obj_node.kind() == "identifier" { | ||
| let obj_name = node_text(&obj_node, source); | ||
| if obj_name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) | ||
| && !is_python_builtin(obj_name) | ||
| { | ||
| type_map.push(TypeMapEntry { | ||
| name: var_name, | ||
| type_name: obj_name.to_string(), | ||
| confidence: 0.7, | ||
| }); | ||
|
Comment on lines
+435
to
+448
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Can you confirm the JS extractor also stores the module name (not the class name) in the attribute case, and that downstream resolution benefits from this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed — the JS extractor ( |
||
| } | ||
| } | ||
| } | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
@@ -455,4 +548,65 @@ mod tests { | |
| let c = s.definitions.iter().find(|d| d.name == "MAX_RETRIES").unwrap(); | ||
| assert_eq!(c.kind, "constant"); | ||
| } | ||
|
|
||
| // ── Assignment typeMap tests ───────────────────────────────────────────── | ||
|
|
||
| #[test] | ||
| fn infers_constructor_call_uppercase() { | ||
| // order = Order("o1", 100.0) → order : Order at conf 1.0 | ||
| let s = parse_py("def run():\n order = Order(\"o1\", 100.0)\n order.validate()\n"); | ||
| let entry = s.type_map.iter().find(|e| e.name == "order"); | ||
| assert!(entry.is_some(), "expected order in type_map"); | ||
| let entry = entry.unwrap(); | ||
| assert_eq!(entry.type_name, "Order"); | ||
| assert!((entry.confidence - 1.0).abs() < f64::EPSILON); | ||
| } | ||
|
|
||
| #[test] | ||
| fn infers_module_factory_call() { | ||
| // svc = Models.UserService(db) → svc : Models at conf 0.7 | ||
| // The object name must be uppercase to match the JS heuristic. | ||
| let s = parse_py("def run():\n svc = Models.UserService(db)\n svc.create()\n"); | ||
| let entry = s.type_map.iter().find(|e| e.name == "svc"); | ||
| assert!(entry.is_some(), "expected svc in type_map for Module.Class(...)"); | ||
| let entry = entry.unwrap(); | ||
| assert_eq!(entry.type_name, "Models"); | ||
| assert!((entry.confidence - 0.7).abs() < f64::EPSILON); | ||
| } | ||
|
|
||
| #[test] | ||
| fn does_not_infer_lowercase_module_factory() { | ||
| // svc = models.UserService(db) — lowercase module name → no typeMap entry (matches JS) | ||
| let s = parse_py("def run():\n svc = models.UserService(db)\n svc.create()\n"); | ||
| assert!( | ||
| s.type_map.iter().all(|e| e.name != "svc"), | ||
| "should not seed typeMap for lowercase module prefix" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn does_not_infer_lowercase_constructor() { | ||
| // obj = create_thing() — lowercase, should not seed typeMap | ||
| let s = parse_py("def run():\n obj = create_thing()\n obj.work()\n"); | ||
| assert!( | ||
| s.type_map.iter().all(|e| e.name != "obj"), | ||
| "should not seed typeMap for lowercase function call" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn does_not_infer_builtin_exception() { | ||
| // err = ValueError("msg") — builtin exception, should not seed typeMap | ||
| let s = parse_py("def run():\n err = ValueError(\"msg\")\n"); | ||
| // Note: ValueError is uppercase so it WOULD match the heuristic — but it's a builtin. | ||
| // The JS extractor does NOT exclude builtins from conf-1.0 uppercase constructor | ||
| // matching (only from the attribute/factory path). We match that behaviour here. | ||
| // This test documents the current behaviour rather than asserting exclusion. | ||
| let entry = s.type_map.iter().find(|e| e.name == "err"); | ||
| // Builtins ARE seeded at conf 1.0 by the identifier branch (same as JS). | ||
| // Only the attribute/factory branch (Module.Class) checks is_python_builtin. | ||
| if let Some(e) = entry { | ||
| assert_eq!(e.type_name, "ValueError"); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
infer_address_of_compositeonly checks that theunary_expression's operand is acomposite_literal, but never verifies the operator is&. In theory, any other unary operator applied to a composite literal (e.g. a hypothetical^Struct{}) would still seed the typeMap. While valid Go syntax prevents non-&unary ops on composite literals from compiling, the function operates on the raw AST, so a defensive check on the operator node would make the intent explicit and guard against future grammar edge-cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — added a defensive check for the
&operator before treating aunary_expressionas address-of:if node_text(&op_node, source) != "&" { return false; }. This makes the intent explicit and guards against future grammar edge cases.