fix: PHP namespace block extraction, C/C++ language split, and Lua config

- Handle block-scoped PHP namespaces (`namespace Foo { class Bar {} }`)
  by recursing into compound_statement bodies in PhpExtractor
- Separate C (.c/.h) from C++ (.cpp/.cc/.hpp) into distinct language
  configs so .c/.h files resolve to language "c" instead of "cpp"
- Add Lua language config so .lua files resolve to "lua" instead of
  "unknown" after the EXTENSION_LANGUAGE map was replaced by LanguageRegistry
- Update TreeSitterPlugin JSDoc to reflect all 10 supported languages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-16 14:40:24 +08:00
co-authored by Claude Opus 4.6
parent 3f07df056f
commit a2907ec76f
10 changed files with 173 additions and 15 deletions
@@ -49,10 +49,10 @@ describe("LanguageRegistry", () => {
});
describe("createDefault", () => {
it("registers all 38 built-in language configs", () => {
it("registers all 40 built-in language configs", () => {
const registry = LanguageRegistry.createDefault();
const all = registry.getAllLanguages();
expect(all.length).toBe(38);
expect(all.length).toBe(40);
});
it("maps all expected extensions", () => {
@@ -68,6 +68,9 @@ describe("LanguageRegistry", () => {
expect(registry.getByExtension(".kt")?.id).toBe("kotlin");
expect(registry.getByExtension(".cs")?.id).toBe("csharp");
expect(registry.getByExtension(".cpp")?.id).toBe("cpp");
expect(registry.getByExtension(".c")?.id).toBe("c");
expect(registry.getByExtension(".h")?.id).toBe("c");
expect(registry.getByExtension(".lua")?.id).toBe("lua");
expect(registry.getByExtension(".js")?.id).toBe("javascript");
});
@@ -0,0 +1,27 @@
import type { LanguageConfig } from "../types.js";
export const cConfig = {
id: "c",
displayName: "C",
extensions: [".c", ".h"],
treeSitter: {
wasmPackage: "tree-sitter-cpp",
wasmFile: "tree-sitter-cpp.wasm",
},
concepts: [
"pointers",
"manual memory management",
"structs",
"unions",
"function pointers",
"preprocessor macros",
"header files",
"static vs dynamic linking",
],
filePatterns: {
entryPoints: ["main.c", "src/main.c"],
barrels: [],
tests: ["*_test.c", "test_*.c"],
config: ["Makefile", "CMakeLists.txt", "meson.build"],
},
} satisfies LanguageConfig;
@@ -2,8 +2,8 @@ import type { LanguageConfig } from "../types.js";
export const cppConfig = {
id: "cpp",
displayName: "C/C++",
extensions: [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"],
displayName: "C++",
extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hxx"],
treeSitter: {
wasmPackage: "tree-sitter-cpp",
wasmFile: "tree-sitter-cpp.wasm",
@@ -21,7 +21,7 @@ export const cppConfig = {
"STL containers",
],
filePatterns: {
entryPoints: ["main.cpp", "main.c", "src/main.cpp"],
entryPoints: ["main.cpp", "src/main.cpp"],
barrels: [],
tests: ["*_test.cpp", "*_test.cc", "test_*.cpp"],
config: ["CMakeLists.txt", "Makefile", "meson.build"],
@@ -9,8 +9,10 @@ import { rubyConfig } from "./ruby.js";
import { phpConfig } from "./php.js";
import { swiftConfig } from "./swift.js";
import { kotlinConfig } from "./kotlin.js";
import { cConfig } from "./c.js";
import { cppConfig } from "./cpp.js";
import { csharpConfig } from "./csharp.js";
import { luaConfig } from "./lua.js";
// Non-code language configs
import { markdownConfig } from "./markdown.js";
import { yamlConfig } from "./yaml.js";
@@ -51,6 +53,8 @@ export const builtinLanguageConfigs: LanguageConfig[] = [
phpConfig,
swiftConfig,
kotlinConfig,
luaConfig,
cConfig,
cppConfig,
csharpConfig,
// Non-code languages
@@ -94,6 +98,8 @@ export {
phpConfig,
swiftConfig,
kotlinConfig,
luaConfig,
cConfig,
cppConfig,
csharpConfig,
// Non-code languages
@@ -0,0 +1,23 @@
import type { LanguageConfig } from "../types.js";
export const luaConfig = {
id: "lua",
displayName: "Lua",
extensions: [".lua"],
concepts: [
"tables",
"metatables",
"coroutines",
"closures",
"prototype-based OOP",
"varargs",
"weak references",
"environments",
],
filePatterns: {
entryPoints: ["main.lua", "init.lua"],
barrels: [],
tests: ["*_test.lua", "test_*.lua", "*_spec.lua"],
config: [".luacheckrc", "rockspec"],
},
} satisfies LanguageConfig;
@@ -32,7 +32,7 @@ describe("CppExtractor", () => {
const extractor = new CppExtractor();
it("has correct languageIds", () => {
expect(extractor.languageIds).toEqual(["cpp"]);
expect(extractor.languageIds).toEqual(["cpp", "c"]);
});
// ---- Functions ----
@@ -583,6 +583,78 @@ function helper(string $x): string {
});
});
// ---- Block-scoped namespaces ----
describe("extractStructure - block-scoped namespaces", () => {
it("extracts classes and functions inside block-scoped namespaces", () => {
const { tree, parser, root } = parse(`<?php
namespace App\\Controllers {
class UserController {
public function index(): void {}
}
function helperInNs(): string {
return "ok";
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("UserController");
expect(result.classes[0].methods).toContain("index");
expect(result.functions.some((f) => f.name === "helperInNs")).toBe(true);
expect(result.functions.some((f) => f.name === "index")).toBe(true);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("UserController");
expect(exportNames).toContain("helperInNs");
tree.delete();
parser.delete();
});
it("extracts interfaces inside block-scoped namespaces", () => {
const { tree, parser, root } = parse(`<?php
namespace App\\Contracts {
interface Repository {
public function find(int $id): mixed;
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Repository");
expect(result.classes[0].methods).toContain("find");
tree.delete();
parser.delete();
});
it("extracts use statements inside block-scoped namespaces", () => {
const { tree, parser, root } = parse(`<?php
namespace App\\Services {
use App\\Models\\User;
class UserService {
public function get(): void {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(1);
expect(result.imports[0].source).toBe("App\\Models\\User");
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("UserService");
tree.delete();
parser.delete();
});
});
// ---- Nullable return types ----
describe("nullable return types", () => {
@@ -122,7 +122,7 @@ function isStatic(node: TreeSitterNode): boolean {
* public class/struct members are treated as exports.
*/
export class CppExtractor implements LanguageExtractor {
readonly languageIds = ["cpp"];
readonly languageIds = ["cpp", "c"];
extractStructure(rootNode: TreeSitterNode): StructuralAnalysis {
const functions: StructuralAnalysis["functions"] = [];
@@ -124,8 +124,25 @@ export class PhpExtractor implements LanguageExtractor {
// tree-sitter-php wraps everything under `program`. The children of
// `program` include `php_tag`, `namespace_definition`, `namespace_use_declaration`,
// `class_declaration`, `function_definition`, etc.
for (let i = 0; i < rootNode.childCount; i++) {
const node = rootNode.child(i);
this.walkStatements(rootNode, functions, classes, imports, exports);
return { functions, classes, imports, exports };
}
/**
* Walk top-level statements, extracting functions, classes, interfaces, and imports.
* Handles both direct children and declarations nested inside block-scoped
* `namespace_definition` nodes (`namespace Foo { class Bar {} }`).
*/
private walkStatements(
parent: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
imports: StructuralAnalysis["imports"],
exports: StructuralAnalysis["exports"],
): void {
for (let i = 0; i < parent.childCount; i++) {
const node = parent.child(i);
if (!node) continue;
switch (node.type) {
@@ -156,10 +173,19 @@ export class PhpExtractor implements LanguageExtractor {
case "namespace_use_declaration":
this.extractUseDeclaration(node, imports);
break;
case "namespace_definition": {
// Block-scoped namespaces (`namespace Foo { ... }`) nest declarations
// inside a compound_statement body. Declarative namespaces (`namespace Foo;`)
// have no body — their declarations are already siblings at the root.
const body = findChild(node, "compound_statement");
if (body) {
this.walkStatements(body, functions, classes, imports, exports);
}
break;
}
}
}
return { functions, classes, imports, exports };
}
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
@@ -21,9 +21,10 @@ type TreeSitterNode = import("web-tree-sitter").Node;
* Config-driven tree-sitter plugin.
*
* Accepts LanguageConfig objects to determine which languages to support
* and how to load their WASM grammars. Currently provides deep structural
* analysis for TypeScript/JavaScript; other languages with tree-sitter configs
* get basic function/class/import extraction.
* and how to load their WASM grammars. Provides deep structural analysis
* (functions, classes, imports, exports, call graphs) for all languages
* with registered extractors: TypeScript, JavaScript, Python, Go, Rust,
* Java, Ruby, PHP, C/C++, and C#.
*
* Languages without tree-sitter configs are gracefully skipped (the LLM
* agent handles analysis for those).
@@ -51,7 +52,7 @@ export class TreeSitterPlugin implements AnalyzerPlugin {
* If no configs are provided, defaults to TypeScript and JavaScript.
*
* @param configs Language configurations to load
* @param extractors Optional language extractors; if none provided, registers TypeScriptExtractor by default
* @param extractors Optional language extractors; if none provided, registers all builtin extractors
*/
constructor(configs?: LanguageConfig[], extractors?: LanguageExtractor[]) {
if (configs) {