Skip to content

Commit 228edc7

Browse files
committed
v0.4: Pipeline quality improvements, snippet tool optimization
Pipeline quality: - 17 import parsers (ES modules, Java, Kotlin, Scala, C#, C, C++, PHP, Ruby, Rust, Lua, Elixir, Bash, Zig, Erlang, Haskell, OCaml) - Language-aware isClassDeclaration() for TS, Java, C#, Scala, Kotlin, PHP - Route test filtering (isTestNode + containsTestSegment) - Haskell/OCaml/Elixir callee extraction for apply/infix/application nodes - JSX component refs via extractJSXComponentRefs() - Typed + dynamic type inference split - Extraction test coverage (1679 lines) Snippet tool optimization for AI coding agents: - Replace "error" key with "status"/"message" in disambiguation responses - Add auto_resolve param (opt-in, picks best from <=2 candidates) - Add include_neighbors param (opt-in, returns caller/callee names) - Fix fuzzy fallback to extract last dot-segment from qualified names - Add NodeNeighborNames store method for neighbor name lookups
1 parent 8e3870f commit 228edc7

29 files changed

Lines changed: 6654 additions & 193 deletions

internal/httplink/httplink.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,31 @@ func (l *Linker) linkHandlerToRoute(handlerNode *store.Node, routeID int64, rout
313313
}
314314
}
315315

316+
// isTestNode returns true if the node is from a test file.
317+
// Checks the is_test property set during pipeline pass 1, with a file path heuristic fallback.
318+
func isTestNode(n *store.Node) bool {
319+
if isTest, ok := n.Properties["is_test"].(bool); ok && isTest {
320+
return true
321+
}
322+
// Fallback: common test path patterns
323+
fp := filepath.ToSlash(n.FilePath)
324+
return containsTestSegment(fp, "test") ||
325+
containsTestSegment(fp, "tests") ||
326+
containsTestSegment(fp, "__tests__") ||
327+
strings.Contains(fp, "_test.") ||
328+
strings.Contains(fp, ".test.") ||
329+
strings.Contains(fp, ".spec.")
330+
}
331+
332+
// containsTestSegment checks if a path contains a directory segment named seg.
333+
// Matches both "seg/..." (at start) and ".../seg/..." (mid-path).
334+
func containsTestSegment(fp, seg string) bool {
335+
return strings.HasPrefix(fp, seg+"/") || strings.Contains(fp, "/"+seg+"/")
336+
}
337+
316338
// discoverRoutes finds route handler registrations from Function nodes.
339+
//
340+
//nolint:gocognit // WHY: inherent complexity from multi-framework route discovery
317341
func (l *Linker) discoverRoutes(rootPath string) []RouteHandler {
318342
var routes []RouteHandler
319343

@@ -334,6 +358,11 @@ func (l *Linker) discoverRoutes(rootPath string) []RouteHandler {
334358
phpFilesWithFuncs := map[string]bool{}
335359

336360
for _, f := range funcs {
361+
// Skip test files — test fixtures should not produce Route nodes
362+
if isTestNode(f) {
363+
continue
364+
}
365+
337366
// Python: check decorators property
338367
routes = append(routes, extractPythonRoutes(f)...)
339368

@@ -371,6 +400,11 @@ func (l *Linker) discoverRoutes(rootPath string) []RouteHandler {
371400
}
372401

373402
for _, m := range modules {
403+
// Skip test files
404+
if isTestNode(m) {
405+
continue
406+
}
407+
374408
isPHP := strings.HasSuffix(m.FilePath, ".php")
375409
isJSTS := strings.HasSuffix(m.FilePath, ".js") ||
376410
strings.HasSuffix(m.FilePath, ".ts") ||

internal/httplink/httplink_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,3 +1441,30 @@ Route::get('/api/orders/{id}', [OrderController::class, 'show']);
14411441
}
14421442
}
14431443
}
1444+
1445+
func TestIsTestNodeFiltering(t *testing.T) {
1446+
tests := []struct {
1447+
filePath string
1448+
isTest bool
1449+
expected bool
1450+
}{
1451+
{"src/routes/api.js", false, false},
1452+
{"test/app.get.js", false, true},
1453+
{"__tests__/routes.test.ts", false, true},
1454+
{"src/routes/api.js", true, true},
1455+
{"lib/router/index.js", false, false},
1456+
{"tests/fixtures/server.js", false, true},
1457+
{"app/controllers/orders_controller.rb", false, false},
1458+
}
1459+
1460+
for _, tt := range tests {
1461+
n := &store.Node{
1462+
FilePath: tt.filePath,
1463+
Properties: map[string]any{"is_test": tt.isTest},
1464+
}
1465+
got := isTestNode(n)
1466+
if got != tt.expected {
1467+
t.Errorf("isTestNode(%q, is_test=%v) = %v, want %v", tt.filePath, tt.isTest, got, tt.expected)
1468+
}
1469+
}
1470+
}

internal/lang/elixir.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func init() {
1212
},
1313
ClassNodeTypes: []string{}, // handled by custom extraction (defmodule)
1414
ModuleNodeTypes: []string{"source"},
15-
CallNodeTypes: []string{"call", "dot"},
15+
CallNodeTypes: []string{"call", "dot", "binary_operator"},
1616
ImportNodeTypes: []string{"call"},
1717

1818
BranchingNodeTypes: []string{

internal/lang/ocaml.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func init() {
1313
"module_definition", // module M = struct ... end
1414
},
1515
ModuleNodeTypes: []string{"compilation_unit"},
16-
CallNodeTypes: []string{"application", "infix_expression"},
16+
CallNodeTypes: []string{"application_expression", "infix_expression"},
1717
ImportNodeTypes: []string{"open_module"},
1818

1919
BranchingNodeTypes: []string{

internal/lang/perl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ func init() {
1010
ClassNodeTypes: []string{},
1111
FieldNodeTypes: []string{},
1212
ModuleNodeTypes: []string{"source_file"},
13-
CallNodeTypes: []string{"ambiguous_function_call_expression", "function_call_expression"},
13+
CallNodeTypes: []string{"ambiguous_function_call_expression", "function_call_expression", "func1op_call_expression"},
1414
ImportNodeTypes: []string{"use_statement", "require_statement"},
1515
BranchingNodeTypes: []string{
1616
"if_statement", "unless_statement", "for_statement",

internal/lang/sql.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ func init() {
1010
},
1111
ClassNodeTypes: []string{},
1212
FieldNodeTypes: []string{"column_definition"},
13-
CallNodeTypes: []string{"function_call"},
13+
CallNodeTypes: []string{"function_call", "invocation"},
1414
ImportNodeTypes: []string{},
1515
VariableNodeTypes: []string{
1616
"create_table",

internal/pipeline/astdump_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,107 @@ var astDumpCases = []struct {
301301
{"sql_func_call", lang.SQL, "SELECT count(*), upper(name) FROM users;\n"},
302302
{"sql_view", lang.SQL, "CREATE VIEW active_users AS SELECT * FROM users WHERE active = true;\n"},
303303

304+
// === C# call AST diagnostics ===
305+
{"csharp_member_call", lang.CSharp, "class A {\n\tvoid F() {\n\t\t_context.SaveChangesAsync(token);\n\t\tentity.AddDomainEvent(new Event());\n\t\tvar x = _identityService.GetUserNameAsync(userId);\n\t}\n}\n"},
306+
{"csharp_static_call", lang.CSharp, "class A {\n\tvoid F() {\n\t\tConsole.WriteLine(\"hello\");\n\t\tstring.IsNullOrEmpty(s);\n\t}\n}\n"},
307+
{"csharp_simple_call", lang.CSharp, "class A {\n\tvoid F() {\n\t\tDoWork();\n\t\tawait SaveAsync();\n\t}\n}\n"},
308+
{"csharp_await_call", lang.CSharp, "class A {\n\tasync Task F() {\n\t\tawait _context.SaveChangesAsync(token);\n\t\tvar result = await next();\n\t}\n}\n"},
309+
{"csharp_new_object", lang.CSharp, "class A {\n\tvoid F() {\n\t\tvar e = new TodoItem();\n\t\tvar list = new List<string>();\n\t}\n}\n"},
310+
311+
// === Ruby call AST diagnostics ===
312+
{"ruby_method_call", lang.Ruby, "class A\n def f\n name.to_s\n @items.each { |i| puts i }\n response.status = 200\n end\nend\n"},
313+
{"ruby_bare_call", lang.Ruby, "class A\n def f\n puts 'hello'\n greet('world')\n render json: data\n end\nend\n"},
314+
{"ruby_chain_call", lang.Ruby, "class A\n def f\n users.where(active: true).order(:name).limit(10)\n end\nend\n"},
315+
304316
// === New languages: Dockerfile ===
305317
{"dockerfile_from", lang.Dockerfile, "FROM golang:1.22-alpine AS builder\nWORKDIR /app\nCOPY . .\nRUN go build -o main .\n"},
306318
{"dockerfile_env", lang.Dockerfile, "ENV APP_PORT=8080\nARG VERSION=latest\nEXPOSE 8080\n"},
319+
320+
// =====================================================================
321+
// v0.5 Documentation Cases — Complex patterns for extraction planning
322+
// =====================================================================
323+
324+
// === Framework Patterns: Express/Gin/Chi routes ===
325+
{"js_express_route", lang.JavaScript, "app.get('/users/:id', async (req, res) => {\n const user = await db.findUser(req.params.id);\n res.json(user);\n});\n"},
326+
{"js_express_middleware", lang.JavaScript, "app.use('/api', authMiddleware, rateLimit({ max: 100 }));\n"},
327+
{"ts_express_typed_route", lang.TypeScript, "router.post('/orders', validate(OrderSchema), async (req: Request, res: Response) => {\n const order = await orderService.create(req.body);\n res.status(201).json(order);\n});\n"},
328+
{"go_chi_route", lang.Go, "package main\nfunc routes(r chi.Router) {\n\tr.Get(\"/users/{id}\", getUser)\n\tr.Post(\"/users\", createUser)\n\tr.Route(\"/admin\", func(r chi.Router) {\n\t\tr.Use(adminOnly)\n\t\tr.Get(\"/stats\", getStats)\n\t})\n}\n"},
329+
330+
// === Framework Patterns: Spring/Django/Laravel annotations ===
331+
{"java_spring_controller", lang.Java, "@RestController\n@RequestMapping(\"/api/users\")\nclass UserController {\n\t@Autowired\n\tprivate UserService userService;\n\t@GetMapping(\"/{id}\")\n\tpublic User getUser(@PathVariable Long id) {\n\t\treturn userService.findById(id);\n\t}\n}\n"},
332+
{"java_spring_service", lang.Java, "@Service\nclass UserService {\n\t@Transactional\n\tpublic User createUser(CreateUserRequest req) {\n\t\tUser user = new User(req.getName());\n\t\treturn repository.save(user);\n\t}\n}\n"},
333+
{"php_laravel_route", lang.PHP, "<?php\nRoute::middleware('auth')->group(function () {\n\tRoute::get('/dashboard', [DashboardController::class, 'index']);\n\tRoute::resource('posts', PostController::class);\n});\n"},
334+
335+
// === Framework Patterns: Ruby Sinatra/Rails DSL ===
336+
{"ruby_sinatra_route", lang.Ruby, "class App < Sinatra::Base\n get '/users/:id' do\n user = User.find(params[:id])\n json user\n end\n\n post '/users' do\n user = User.create(params)\n status 201\n json user\n end\nend\n"},
337+
{"ruby_rails_model", lang.Ruby, "class User < ApplicationRecord\n has_many :posts, dependent: :destroy\n belongs_to :organization\n validates :email, presence: true, uniqueness: true\n scope :active, -> { where(active: true) }\nend\n"},
338+
339+
// === Framework Patterns: Elixir Phoenix ===
340+
{"elixir_phoenix_router", lang.Elixir, "defmodule MyAppWeb.Router do\n use MyAppWeb, :router\n pipeline :api do\n plug :accepts, [\"json\"]\n end\n scope \"/api\", MyAppWeb do\n pipe_through :api\n resources \"/users\", UserController\n end\nend\n"},
341+
342+
// === Async Patterns ===
343+
{"ts_async_await", lang.TypeScript, "async function fetchData(): Promise<User[]> {\n const response = await fetch('/api/users');\n if (!response.ok) throw new Error('Failed');\n return await response.json();\n}\n"},
344+
{"rust_async_await", lang.Rust, "async fn fetch_user(id: u64) -> Result<User, Error> {\n\tlet resp = client.get(format!(\"/users/{}\", id)).send().await?;\n\tlet user: User = resp.json().await?;\n\tOk(user)\n}\n"},
345+
{"python_async_gather", lang.Python, "async def fetch_all(ids):\n tasks = [fetch_user(id) for id in ids]\n results = await asyncio.gather(*tasks)\n return results\n"},
346+
{"kotlin_coroutine", lang.Kotlin, "suspend fun fetchUsers(): List<User> {\n\treturn withContext(Dispatchers.IO) {\n\t\tval response = api.getUsers()\n\t\tresponse.body() ?: emptyList()\n\t}\n}\n"},
347+
{"go_goroutine_channel", lang.Go, "package main\nfunc process(ctx context.Context, items []Item) error {\n\terrCh := make(chan error, len(items))\n\tfor _, item := range items {\n\t\tgo func(it Item) {\n\t\t\terrCh <- handle(ctx, it)\n\t\t}(item)\n\t}\n\tfor range items {\n\t\tif err := <-errCh; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"},
348+
349+
// === Error Handling Patterns ===
350+
{"ts_try_catch_finally", lang.TypeScript, "async function safeFetch(url: string): Promise<Data | null> {\n try {\n const res = await fetch(url);\n return await res.json();\n } catch (err) {\n logger.error('fetch failed', { url, err });\n return null;\n } finally {\n metrics.recordFetch(url);\n }\n}\n"},
351+
{"rust_question_mark_chain", lang.Rust, "fn process(path: &str) -> Result<Config, Error> {\n\tlet data = std::fs::read_to_string(path)?;\n\tlet config: Config = serde_json::from_str(&data)?;\n\tconfig.validate()?;\n\tOk(config)\n}\n"},
352+
{"go_error_wrap", lang.Go, "package main\nimport \"fmt\"\nfunc loadConfig(path string) (*Config, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read config: %w\", err)\n\t}\n\tvar cfg Config\n\tif err := json.Unmarshal(data, &cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse config: %w\", err)\n\t}\n\treturn &cfg, nil\n}\n"},
353+
{"python_exception_chain", lang.Python, "def process(data):\n try:\n result = validate(data)\n except ValidationError as e:\n raise ProcessingError(f'invalid: {e}') from e\n except TimeoutError:\n raise RetryableError('timeout') from None\n"},
354+
{"java_multi_catch", lang.Java, "class A {\n\tvoid process() {\n\t\ttry {\n\t\t\triskyOperation();\n\t\t} catch (IOException | SQLException e) {\n\t\t\tlogger.error(\"Operation failed\", e);\n\t\t\tthrow new ServiceException(e);\n\t\t} finally {\n\t\t\tcleanup();\n\t\t}\n\t}\n}\n"},
355+
{"elixir_with_error", lang.Elixir, "defmodule A do\n def process(params) do\n with {:ok, user} <- find_user(params.id),\n {:ok, order} <- create_order(user, params),\n :ok <- send_notification(user, order) do\n {:ok, order}\n else\n {:error, :not_found} -> {:error, \"user not found\"}\n {:error, reason} -> {:error, reason}\n end\n end\nend\n"},
356+
357+
// === Generic/Template Patterns ===
358+
{"ts_generic_class", lang.TypeScript, "class Repository<T extends Entity> {\n constructor(private readonly db: Database) {}\n async findById(id: string): Promise<T | null> {\n return this.db.collection<T>().findOne({ id });\n }\n}\n"},
359+
{"java_generic_method", lang.Java, "class A {\n\t<T extends Comparable<T>> List<T> sort(List<T> items) {\n\t\treturn items.stream().sorted().collect(Collectors.toList());\n\t}\n}\n"},
360+
{"rust_generic_impl", lang.Rust, "impl<T: Clone + Send + 'static> From<Vec<T>> for Collection<T> {\n\tfn from(items: Vec<T>) -> Self {\n\t\tSelf { items, len: items.len() }\n\t}\n}\n"},
361+
{"cpp_template_class", lang.CPP, "template<typename T, typename Alloc = std::allocator<T>>\nclass Vector {\npublic:\n\tvoid push_back(const T& value) { data_.push_back(value); }\n\tT& operator[](size_t index) { return data_[index]; }\nprivate:\n\tstd::vector<T, Alloc> data_;\n};\n"},
362+
{"scala_type_bounds", lang.Scala, "object A {\n\tdef process[T <: Serializable : Ordering](items: List[T]): List[T] = {\n\t\titems.sorted.distinct\n\t}\n}\n"},
363+
364+
// === Destructuring Patterns ===
365+
{"js_object_destructure", lang.JavaScript, "const { name, age, address: { city } } = getUserInfo();\nconst [first, ...rest] = getItems();\n"},
366+
{"ts_function_destructure", lang.TypeScript, "function processUser({ id, name, roles = [] }: UserInput): Result {\n return { id, displayName: name, isAdmin: roles.includes('admin') };\n}\n"},
367+
{"python_unpack", lang.Python, "first, *middle, last = get_items()\n(x, y), z = get_coords()\nname, age = user_info.values()\n"},
368+
{"rust_pattern_match", lang.Rust, "fn handle(msg: Message) {\n\tmatch msg {\n\t\tMessage::Quit => println!(\"quit\"),\n\t\tMessage::Move { x, y } => move_to(x, y),\n\t\tMessage::Write(text) => println!(\"{}\", text),\n\t\tMessage::Color(r, g, b) => set_color(r, g, b),\n\t}\n}\n"},
369+
{"kotlin_destructure", lang.Kotlin, "fun process(pair: Pair<String, Int>) {\n\tval (name, age) = pair\n\tval (first, second, third) = Triple(1, 2, 3)\n}\n"},
370+
371+
// === Metaprogramming/Dynamic Patterns ===
372+
{"python_decorator_stack", lang.Python, "@app.route('/users', methods=['GET'])\n@login_required\n@cache(timeout=300)\ndef list_users():\n return User.query.all()\n"},
373+
{"ruby_method_missing", lang.Ruby, "class DynamicProxy\n def method_missing(name, *args, &block)\n if target.respond_to?(name)\n target.send(name, *args, &block)\n else\n super\n end\n end\nend\n"},
374+
{"python_dunder_methods", lang.Python, "class Vector:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n def __add__(self, other):\n return Vector(self.x + other.x, self.y + other.y)\n def __repr__(self):\n return f'Vector({self.x}, {self.y})'\n"},
375+
{"ts_mapped_types", lang.TypeScript, "type Readonly<T> = { readonly [P in keyof T]: T[P] };\ntype Optional<T> = { [P in keyof T]?: T[P] };\ntype Pick<T, K extends keyof T> = { [P in K]: T[P] };\n"},
376+
377+
// === Pipe/Composition Patterns ===
378+
{"elixir_pipe_complex", lang.Elixir, "defmodule A do\n def process(data) do\n data\n |> Enum.filter(&(&1.active))\n |> Enum.map(&transform/1)\n |> Enum.sort_by(& &1.priority)\n |> Enum.take(10)\n end\nend\n"},
379+
{"haskell_composition", lang.Haskell, "processAll = map (show . succ . abs)\n"},
380+
{"ocaml_pipe_complex", lang.OCaml, "let process items =\n items\n |> List.filter (fun x -> x > 0)\n |> List.map (fun x -> x * 2)\n |> List.sort compare\n"},
381+
382+
// === Interface/Trait Implementation Patterns ===
383+
{"rust_trait_impl", lang.Rust, "trait Handler {\n\tfn handle(&self, req: &Request) -> Response;\n}\nstruct ApiHandler { db: Database }\nimpl Handler for ApiHandler {\n\tfn handle(&self, req: &Request) -> Response {\n\t\tself.db.query(req.path())\n\t}\n}\n"},
384+
{"go_interface_impl", lang.Go, "package main\ntype Handler interface {\n\tHandle(ctx context.Context, req *Request) (*Response, error)\n}\ntype APIHandler struct{ db *DB }\nfunc (h *APIHandler) Handle(ctx context.Context, req *Request) (*Response, error) {\n\treturn h.db.Query(ctx, req.Path)\n}\n"},
385+
{"csharp_interface_impl", lang.CSharp, "interface IHandler {\n\tTask<Response> HandleAsync(Request req);\n}\nclass ApiHandler : IHandler {\n\tprivate readonly IDb _db;\n\tpublic async Task<Response> HandleAsync(Request req) {\n\t\treturn await _db.QueryAsync(req.Path);\n\t}\n}\n"},
386+
{"kotlin_interface_impl", lang.Kotlin, "interface Handler {\n\tsuspend fun handle(req: Request): Response\n}\nclass ApiHandler(private val db: Database) : Handler {\n\toverride suspend fun handle(req: Request): Response {\n\t\treturn db.query(req.path)\n\t}\n}\n"},
387+
388+
// === Closure/Lambda Patterns ===
389+
{"go_closure_capture", lang.Go, "package main\nfunc makeCounter() func() int {\n\tcount := 0\n\treturn func() int {\n\t\tcount++\n\t\treturn count\n\t}\n}\n"},
390+
{"rust_closure_move", lang.Rust, "fn spawn_task(data: Vec<u8>) {\n\ttokio::spawn(async move {\n\t\tprocess(&data).await;\n\t\tprintln!(\"done\");\n\t});\n}\n"},
391+
{"java_lambda_stream", lang.Java, "class A {\n\tList<String> process(List<User> users) {\n\t\treturn users.stream()\n\t\t\t.filter(u -> u.isActive())\n\t\t\t.map(User::getName)\n\t\t\t.sorted()\n\t\t\t.collect(Collectors.toList());\n\t}\n}\n"},
392+
393+
// === Erlang OTP Patterns ===
394+
{"erlang_gen_server", lang.Erlang, "-module(counter).\n-behaviour(gen_server).\n-export([start_link/0, init/1, handle_call/3]).\nstart_link() -> gen_server:start_link(?MODULE, 0, []).\ninit(Count) -> {ok, Count}.\nhandle_call(increment, _From, Count) -> {reply, Count + 1, Count + 1}.\n"},
395+
{"erlang_supervisor", lang.Erlang, "-module(my_sup).\n-behaviour(supervisor).\n-export([start_link/0, init/1]).\nstart_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).\ninit([]) -> {ok, {{one_for_one, 5, 10}, [{worker, {my_worker, start_link, []}, permanent, 5000, worker, [my_worker]}]}}.\n"},
396+
397+
// === Haskell Type Classes ===
398+
{"haskell_typeclass_def", lang.Haskell, "class Container f where\n empty :: f a\n insert :: a -> f a -> f a\n member :: Eq a => a -> f a -> Bool\n"},
399+
{"haskell_instance_impl", lang.Haskell, "instance Show Color where\n show Red = \"red\"\n show Green = \"green\"\n show Blue = \"blue\"\n"},
400+
401+
// === Complex OOP Patterns ===
402+
{"csharp_generic_constraint", lang.CSharp, "class Repository<T> where T : class, IEntity, new() {\n\tpublic T FindById(int id) {\n\t\treturn _context.Set<T>().Find(id);\n\t}\n}\n"},
403+
{"scala_sealed_trait", lang.Scala, "sealed trait Shape\ncase class Circle(radius: Double) extends Shape\ncase class Rectangle(w: Double, h: Double) extends Shape\nobject Shape {\n\tdef area(s: Shape): Double = s match {\n\t\tcase Circle(r) => math.Pi * r * r\n\t\tcase Rectangle(w, h) => w * h\n\t}\n}\n"},
404+
{"php_trait_usage", lang.PHP, "<?php\ntrait Timestampable {\n\tpublic function touch(): void {\n\t\t$this->updatedAt = new DateTime();\n\t}\n}\nclass Post implements JsonSerializable {\n\tuse Timestampable;\n\tpublic function jsonSerialize(): mixed {\n\t\treturn ['title' => $this->title];\n\t}\n}\n"},
307405
}
308406

309407
func TestDumpAST(t *testing.T) {

internal/pipeline/decorates.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ func (p *Pipeline) processNodeDecorators(n *store.Node) int {
5353
continue
5454
}
5555

56-
targetQN := p.registry.Resolve(funcName, moduleQN, importMap)
57-
if targetQN == "" {
56+
targetResult := p.registry.Resolve(funcName, moduleQN, importMap)
57+
if targetResult.QualifiedName == "" {
5858
continue
5959
}
60-
targetNode, _ := p.Store.FindNodeByQN(p.ProjectName, targetQN)
60+
targetNode, _ := p.Store.FindNodeByQN(p.ProjectName, targetResult.QualifiedName)
6161
if targetNode == nil {
6262
continue
6363
}

0 commit comments

Comments
 (0)