瀏覽代碼

feat(resolution): local-variable method calls in Lua, Luau, R, Pascal (#1112) (#1113)

Extends the local-variable receiver-type inference (#1108/#1110) to the
remaining supported languages with object-method calls. An empirical
sweep found Objective-C, Svelte, Vue, and Astro already resolved
`localVar.method()` (ObjC via message-send handling; the template langs
ride the TypeScript path), leaving Lua, Luau, R, and Pascal.

Lua/Luau/R were a resolution gap, not extraction: the call ref IS
extracted (`lg:log`, `lg$log`), but (1) the resolver's fast pre-filter
`hasAnyPossibleMatch` only understood `.`/`::` separators, so a `:`/`$`
ref was dropped before any strategy ran, and (2) matchMethodCall only
parsed `.`/`::` receivers with no local-var inference for these langs.
Fixes: pre-filter now checks the member/receiver around `:` and `$`;
matchMethodCall recognizes `lg:log` / `lg$log` and routes them through
the same inference + validated resolveMethodOnType path; and inference
patterns are added for Lua/Luau (`local x = T.new()` / `T()` / `x: T`),
R (`x <- T$new()`), and Pascal (`var x: T` / `x := T.Create`).

Pascal statement-form calls (`obj.Method;`) now resolve via the new
inference pattern. The assignment-RHS parameterless form
(`x := obj.Method`) is deliberately left as a field read by the existing
Pascal extractor — an intentional field-vs-call ambiguity tradeoff — so
it stays out of scope.

Validated with single-file and two-file same-name repros per language
(resolves to the right method; two-file is same-file-correct, #1079).
Adds all four to the local-variable inference test matrix. Full suite
green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 22 小時之前
父節點
當前提交
358f400c40
共有 4 個文件被更改,包括 57 次插入3 次删除
  1. 1 0
      CHANGELOG.md
  2. 8 0
      __tests__/resolution.test.ts
  3. 16 0
      src/resolution/index.ts
  4. 32 3
      src/resolution/name-matcher.ts

+ 1 - 0
CHANGELOG.md

@@ -13,6 +13,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 - Method calls made through a local variable now resolve to the method in many more languages. When code does `const logger = new Logger(); logger.log();` (or the equivalent), CodeGraph infers the local variable's type from its declaration or initializer and links the call to the right method — so these calls now show up in callers, impact/blast-radius, and `codegraph_explore` flow traces instead of being dropped. Previously only C++ handled this; it now also covers TypeScript, JavaScript, Python, Java, C#, Kotlin, Swift, Go, Rust, Dart, Scala, and PHP. (#1108)
 - Ruby method calls made on a receiver (`logger.log`) now record an edge to the method. Previously the Ruby indexer kept only the receiver and discarded the method name, so a method called through a variable or object had no recorded callers and was missing from impact/blast-radius and flow traces; combined with the local-variable type inference above, `logger = Logger.new; logger.log` now links to `Logger#log`. Calls to a class method (`Foo.bar`) and object construction (`Foo.new`) are still recorded too. (#1110)
+- The same local-variable method-call resolution now extends to Lua, Luau, R, and Pascal/Delphi. A method invoked through a local — Lua/Luau `local lg = Logger.new(); lg:log()`, R `lg <- Logger$new(); lg$log()`, or Pascal `var lg: TLogger; ... lg.Log` — now links to the right method instead of being dropped. (#1112)
 
 ### Fixes
 

+ 8 - 0
__tests__/resolution.test.ts

@@ -1683,6 +1683,14 @@ func main() {
         src: `class Logger { def log(): Int = 1 }\nobject A { def use(): Int = { val lg = new Logger(); lg.log() } }\n` },
       { lang: 'Ruby (x = T.new)', file: 'svc.rb',
         src: `class Logger\n  def log\n    1\n  end\nend\ndef use\n  lg = Logger.new\n  lg.log\nend\n` },
+      { lang: 'Lua (x = T.new(); x:log())', file: 'svc.lua',
+        src: `local Logger = {}\nLogger.__index = Logger\nfunction Logger.new() return setmetatable({}, Logger) end\nfunction Logger:log() return 1 end\nlocal function use() local lg = Logger.new(); return lg:log() end\nreturn use\n` },
+      { lang: 'Luau (x = T.new(); x:log())', file: 'svc.luau',
+        src: `local Logger = {}\nLogger.__index = Logger\nfunction Logger.new() return setmetatable({}, Logger) end\nfunction Logger:log(): number return 1 end\nlocal function use(): number local lg = Logger.new(); return lg:log() end\nreturn use\n` },
+      { lang: 'R (x <- T$new(); x$log())', file: 'svc.R',
+        src: `Logger <- R6::R6Class("Logger", public = list(log = function() 1))\nuse <- function() { lg <- Logger$new(); lg$log() }\n` },
+      { lang: 'Pascal (var x: T; x.Method)', file: 'svc.pas',
+        src: `unit A;\ninterface\ntype TLogger = class function Log: Integer; end;\nimplementation\nfunction TLogger.Log: Integer; begin Result := 1; end;\nprocedure Use;\nvar lg: TLogger;\nbegin\n  lg := TLogger.Create;\n  lg.Log;\nend;\nend.\n` },
     ];
 
     for (const c of cases) {

+ 16 - 0
src/resolution/index.ts

@@ -627,6 +627,22 @@ export class ReferenceResolver {
       }
     }
 
+    // Lua/Luau method calls use a single `:` (`lg:log`); R uses `$` (`lg$log`).
+    // Check the member (and receiver) around these separators too, so the ref
+    // isn't dropped here before the method-call resolver ever sees it. The `:`
+    // case is skipped when the name actually contains `::` (handled above).
+    for (const sep of [':', '$']) {
+      if (sep === ':' && name.includes('::')) continue;
+      const sepIdx = name.indexOf(sep);
+      if (sepIdx > 0) {
+        const receiver = name.substring(0, sepIdx);
+        const member = name.substring(sepIdx + 1);
+        if (this.knownNames.has(member) || this.knownNames.has(receiver)) return true;
+        const capitalized = receiver.charAt(0).toUpperCase() + receiver.slice(1);
+        if (this.knownNames.has(capitalized)) return true;
+      }
+    }
+
     // For path-like references (e.g., "snippets/drawer-menu.liquid"), check the filename
     const slashIdx = name.lastIndexOf('/');
     if (slashIdx > 0) {

+ 32 - 3
src/resolution/name-matcher.ts

@@ -1120,6 +1120,22 @@ function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
       return [
         new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger()
       ];
+    case 'lua':
+    case 'luau':
+      return [
+        new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\.new\\b`), // local lg = Logger.new()
+        new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\s*\\(`), // local lg = Logger(...)  (callable table)
+        new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // Luau: local lg: Logger  / typed param
+      ];
+    case 'r':
+      return [
+        new RegExp(`\\b${r}\\b\\s*(?:<-|<<-|=)\\s*([A-Z][\\w.]*)\\$new\\b`), // lg <- Logger$new()  (R6)
+      ];
+    case 'pascal':
+      return [
+        new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w]*)`), // var lg: TLogger  / param lg: TLogger
+        new RegExp(`\\b${r}\\b\\s*:=\\s*([A-Z][\\w.]*)\\.Create\\b`), // lg := TLogger.Create
+      ];
     default:
       return [];
   }
@@ -1196,20 +1212,33 @@ export function matchMethodCall(
   // `Guard.Against.X()`) matched no pattern and never resolved.
   const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
   const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
-
-  const match = dotMatch || colonMatch;
+  // Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
+  // Recognize these receiver/method separators so local-variable receiver-type
+  // inference (#1108) applies to them too — extraction already emits the ref in
+  // this shape, but the resolver otherwise only understood `.` and `::`.
+  const luaColonMatch = (ref.language === 'lua' || ref.language === 'luau')
+    ? ref.referenceName.match(/^([\w.]+):(\w+)$/)
+    : null;
+  const rDollarMatch = ref.language === 'r'
+    ? ref.referenceName.match(/^([\w.]+)\$(\w+)$/)
+    : null;
+
+  const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
   if (!match) {
     return null;
   }
 
   const [, objectOrClass, methodName] = match;
+  // A simple `receiver.method` / `receiver:method` / `receiver$method` shape whose
+  // receiver type we can try to infer from its local declaration.
+  const inferableReceiver = dotMatch || luaColonMatch || rDollarMatch;
 
   // Infer the receiver's type from its local declaration/initializer in the
   // enclosing scope, then resolve the method on that type (#1108). C++ keeps its
   // dedicated inferrer (header scan + `auto`); every other language uses the
   // shared source-based inferrer. resolveMethodOnType validates the method
   // exists on the inferred type, so a mis-inference produces no edge.
-  if (dotMatch) {
+  if (inferableReceiver) {
     const inferredType =
       ref.language === 'cpp'
         ? inferCppReceiverType(objectOrClass!, ref, context)