diff --git a/CHANGELOG.md b/CHANGELOG.md index 70771ee..f9c7372 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 09bb90c..604a7f8 100644 --- a/__tests__/resolution.test.ts +++ b/__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) { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 5dd8047..cd93add 100644 --- a/src/resolution/index.ts +++ b/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) { diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 4c3a293..228ac9f 100644 --- a/src/resolution/name-matcher.ts +++ b/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+)$/); + // 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; + 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)