From 7d6901a6c1819b3ec59251229fb861f66edb653a Mon Sep 17 00:00:00 2001 From: LeBoufty Date: Thu, 10 Jul 2025 10:41:05 +0200 Subject: [PATCH 1/4] Incluse resolver first shot --- .../php/incluseResolver/index.test.ts | 0 .../php/incluseResolver/index.ts | 163 ++++++++++++++++++ .../php/incluseResolver/queries.ts | 34 ++++ .../php/incluseResolver/types.ts | 9 + src/languagePlugins/php/registree/types.ts | 32 ++++ .../php/testFiles/phpFiles/imports.php | 8 + 6 files changed, 246 insertions(+) create mode 100644 src/languagePlugins/php/incluseResolver/index.test.ts create mode 100644 src/languagePlugins/php/incluseResolver/index.ts create mode 100644 src/languagePlugins/php/incluseResolver/queries.ts create mode 100644 src/languagePlugins/php/incluseResolver/types.ts create mode 100644 src/languagePlugins/php/testFiles/phpFiles/imports.php diff --git a/src/languagePlugins/php/incluseResolver/index.test.ts b/src/languagePlugins/php/incluseResolver/index.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/languagePlugins/php/incluseResolver/index.ts b/src/languagePlugins/php/incluseResolver/index.ts new file mode 100644 index 00000000..e318c17b --- /dev/null +++ b/src/languagePlugins/php/incluseResolver/index.ts @@ -0,0 +1,163 @@ +import type { PHPRegistree } from "../registree/index.ts"; +import { type PHPFile, type PHPNode, SymbolNode } from "../registree/types.ts"; +import type { PHPImports } from "./types.ts"; +import { + PHP_INCLUDE_QUERY, + PHP_USE_DETECTION_QUERY, + PHP_USE_QUERY, +} from "./queries.ts"; +import type Parser from "tree-sitter"; +import { dirname } from "@std/path"; + +export class PHPIncluseResolver { + registree: PHPRegistree; + imports: Map; + + constructor(registree: PHPRegistree) { + this.registree = registree; + this.imports = new Map(); + } + + resolveImports(file: PHPFile) { + if (this.imports.has(file.path)) { + return this.imports.get(file.path)!; + } + const useImports = this.#resolveUseDirectives(file); + const includeImports = this.#resolveIncludeDirectives(file); + const imports: PHPImports = { + resolved: new Map([ + ...useImports.resolved, + ...includeImports.resolved, + ]), + unresolved: { + paths: [ + ...useImports.unresolved.paths, + ...includeImports.unresolved.paths, + ], + namespaces: [ + ...useImports.unresolved.namespaces, + ...includeImports.unresolved.namespaces, + ], + }, + }; + this.imports.set(file.path, imports); + return imports; + } + + #resolveUseDirectives(file: PHPFile): PHPImports { + const useDirectives = PHP_USE_DETECTION_QUERY.captures(file.rootNode); + if (!useDirectives) { + throw new Error(`Error when paring use directives for ${file.path}`); + } + const imports: PHPImports = { + resolved: new Map(), + unresolved: { + paths: [], + namespaces: [], + }, + }; + for (const use of useDirectives) { + const useClause = PHP_USE_QUERY.captures(use.node); + if (!useClause) continue; + let name: string | undefined = undefined; + let node: PHPNode | undefined = undefined; + for (const clause of useClause) { + if (clause.name === "alias") { + name = clause.node.text; + } else { + node = this.registree.tree.findNode(clause.node.text); + if (node && !name) { + name = node.name; + } else if (!node && !name) { + name = clause.node.text; + } + } + } + if (node && name) { + if (node instanceof SymbolNode) { + imports.resolved.set(name, node.symbols); + } else { + for (const [k, v] of node.children) { + if (v instanceof SymbolNode) { + imports.resolved.set(k, v.symbols); + } + } + } + } else if (name) { + imports.unresolved.namespaces.push(name); + } + } + return imports; + } + + #splitBinary(binary: Parser.SyntaxNode): Parser.SyntaxNode[] { + const left = binary.childForFieldName("left")!; + const right = binary.childForFieldName("right")!; + const leftArr = []; + const rightArr = []; + if (left.type === "binary_expression") { + leftArr.push(...this.#splitBinary(left)); + } else { + leftArr.push(left); + } + if (right.type === "binary_expression") { + rightArr.push(...this.#splitBinary(right)); + } else { + rightArr.push(right); + } + return [...leftArr, ...rightArr]; + } + + #resolveIncludeDirectives(file: PHPFile) { + const includeDirectives = PHP_INCLUDE_QUERY.captures(file.rootNode); + if (!includeDirectives) { + throw new Error(`Error when parsing include directives for ${file.path}`); + } + const imports: PHPImports = { + resolved: new Map(), + unresolved: { + paths: [], + namespaces: [], + }, + }; + for (const include of includeDirectives) { + if (include.name === "includestr") { + const path = include.node.text; + const importedfile = this.registree.registry.getFile(path, file.path); + if (importedfile) { + for (const [k, v] of importedfile.symbols) { + if (imports.resolved.has(k)) { + imports.resolved.get(k)!.push(...v); + } else { + imports.resolved.set(k, v); + } + } + } else { + imports.unresolved.paths.push(path); + } + } else if (include.name === "includebin") { + const filepath = this.#splitBinary(include.node) + .map((n) => n.text === "__DIR__" ? dirname(file.path) : n.text) + .filter((n) => n !== "") + .map((n) => n.replace(/['"]/g, "")) + .join("/"); + const importedfile = this.registree.registry.getFile( + filepath, + file.path, + ); + if (importedfile) { + for (const [k, v] of importedfile.symbols) { + if (imports.resolved.has(k)) { + imports.resolved.get(k)!.push(...v); + } else { + imports.resolved.set(k, v); + } + } + } else { + imports.unresolved.paths.push(filepath); + } + } + } + return imports; + } +} diff --git a/src/languagePlugins/php/incluseResolver/queries.ts b/src/languagePlugins/php/incluseResolver/queries.ts new file mode 100644 index 00000000..fc0b79e8 --- /dev/null +++ b/src/languagePlugins/php/incluseResolver/queries.ts @@ -0,0 +1,34 @@ +import Parser from "tree-sitter"; +import { phpParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const PHP_USE_DETECTION_QUERY = new Parser.Query( + phpParser.getLanguage(), + ` + (namespace_use_declaration) + `, +); + +export const PHP_USE_QUERY = new Parser.Query( + phpParser.getLanguage(), + ` + (namespace_use_declaration + (namespace_use_clause + . (_) @ns (namespace_aliasing_clause (_) @alias)? + )) + `, +); + +export const PHP_INCLUDE_QUERY = new Parser.Query( + phpParser.getLanguage(), + ` + (include_expression (string (string_content) @includestr)) + (include_once_expression (string (string_content) @includestr)) + (require_expression (string (string_content) @includestr)) + (require_once_expression (string (string_content) @includestr)) + + (include_expression (binary_expression) @includebin) + (include_once_expression (binary_expression) @includebin) + (require_expression (binary_expression) @includebin) + (require_once_expression (binary_expression) @includebin) + `, +); diff --git a/src/languagePlugins/php/incluseResolver/types.ts b/src/languagePlugins/php/incluseResolver/types.ts new file mode 100644 index 00000000..cf05bae4 --- /dev/null +++ b/src/languagePlugins/php/incluseResolver/types.ts @@ -0,0 +1,9 @@ +import type { ExportedSymbol } from "../exportResolver/types.ts"; + +export interface PHPImports { + resolved: Map; + unresolved: { + paths: string[]; + namespaces: string[]; + }; +} diff --git a/src/languagePlugins/php/registree/types.ts b/src/languagePlugins/php/registree/types.ts index 851e9e0e..261b2c46 100644 --- a/src/languagePlugins/php/registree/types.ts +++ b/src/languagePlugins/php/registree/types.ts @@ -4,6 +4,7 @@ import type { ExportedSymbol, } from "../exportResolver/types.ts"; import { PHPExportResolver } from "../exportResolver/index.ts"; +import { dirname, join } from "@std/path"; export interface PHPNode { name: string; @@ -42,6 +43,21 @@ export class PHPTree extends NamespaceNode { } } + findNode(name: string): PHPNode | undefined { + const parts = name.split("\\"); + let current: Map = this.children; + for (const part of parts) { + if (part === "") { + continue; + } + if (!current.has(part)) { + return undefined; + } + current = current.get(part)!.children; + } + return current.get(parts[parts.length - 1]); + } + addNamespaces(namespaces: ExportedNamespace[]) { for (const ns of namespaces) { const nsparts = ns.name.split("\\"); @@ -106,4 +122,20 @@ export class PHPRegistry { symbols, }); } + + getFile(path: string, origin: string): PHPFile | undefined { + const filepaths = Array.from(this.files.keys()); + // 1. Check current file's directory + const sourceDir = dirname(origin); + const pathFromRelative = join(sourceDir, path).replace(/\\/g, "/"); + const corresponding1 = filepaths.find((f) => f === pathFromRelative); + if (corresponding1) { + return this.files.get(corresponding1); + } + // 2. Check from workspace root + const corresponding2 = filepaths.find((f) => f === path); + if (corresponding2) { + return this.files.get(corresponding2); + } + } } diff --git a/src/languagePlugins/php/testFiles/phpFiles/imports.php b/src/languagePlugins/php/testFiles/phpFiles/imports.php new file mode 100644 index 00000000..56ed68a1 --- /dev/null +++ b/src/languagePlugins/php/testFiles/phpFiles/imports.php @@ -0,0 +1,8 @@ + Date: Thu, 10 Jul 2025 10:53:27 +0200 Subject: [PATCH 2/4] Update pom.xml --- examples/java/websocket/pom.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/java/websocket/pom.xml b/examples/java/websocket/pom.xml index b1818be0..7a3494e2 100644 --- a/examples/java/websocket/pom.xml +++ b/examples/java/websocket/pom.xml @@ -1,5 +1,9 @@ - - + + 4.0.0 websocket From f3ae9629ab290a15689f82e38adeea151c1ffb05 Mon Sep 17 00:00:00 2001 From: LeBoufty Date: Thu, 10 Jul 2025 10:54:59 +0200 Subject: [PATCH 3/4] Update lint_test_compile.yml --- .github/workflows/lint_test_compile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_test_compile.yml b/.github/workflows/lint_test_compile.yml index 261475a9..830e8690 100644 --- a/.github/workflows/lint_test_compile.yml +++ b/.github/workflows/lint_test_compile.yml @@ -24,7 +24,7 @@ jobs: run: deno lint - name: Deno fmt (check) - run: deno fmt --check --ignore=examples/java/websocket/**/*.html + run: deno fmt --check --ignore=examples/** tests: runs-on: ubuntu-latest From 28cd6b317a3e66c40277dfe01a90799df4f976b9 Mon Sep 17 00:00:00 2001 From: LeBoufty Date: Fri, 11 Jul 2025 11:17:57 +0200 Subject: [PATCH 4/4] Incluse resolver completed --- .../php/incluseResolver/index.test.ts | 35 +++++++++++++++++++ .../php/incluseResolver/index.ts | 26 ++++++-------- .../php/incluseResolver/queries.ts | 12 ++----- src/languagePlugins/php/registree/types.ts | 22 ++++++++---- .../php/testFiles/constants.ts | 2 ++ .../php/testFiles/phpFiles/imports.php | 8 ----- .../php/testFiles/phpFiles/include.php | 6 ++++ .../php/testFiles/phpFiles/use.php | 8 +++++ 8 files changed, 78 insertions(+), 41 deletions(-) delete mode 100644 src/languagePlugins/php/testFiles/phpFiles/imports.php create mode 100644 src/languagePlugins/php/testFiles/phpFiles/include.php create mode 100644 src/languagePlugins/php/testFiles/phpFiles/use.php diff --git a/src/languagePlugins/php/incluseResolver/index.test.ts b/src/languagePlugins/php/incluseResolver/index.test.ts index e69de29b..012027d7 100644 --- a/src/languagePlugins/php/incluseResolver/index.test.ts +++ b/src/languagePlugins/php/incluseResolver/index.test.ts @@ -0,0 +1,35 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { getPHPFilesMap } from "../testFiles/index.ts"; +import { PHPRegistree } from "../registree/index.ts"; +import { PHPIncluseResolver } from "./index.ts"; +import { INCLUDE, USE } from "../testFiles/constants.ts"; + +describe("PHP Incluse resolver", () => { + const files = getPHPFilesMap(); + const registree = new PHPRegistree(files); + const resolver = new PHPIncluseResolver(registree); + + test("resolves use directives", () => { + const imports = resolver.resolveImports(registree.registry.files.get(USE)!); + expect(imports.unresolved.namespaces.length).toBe(1); + expect(imports.unresolved.namespaces).toContainEqual("I\\Do\\Not\\Exist"); + expect(imports.resolved.get("f")).toBeDefined(); // nested.php + expect(imports.resolved.get("my_function")).toBeDefined(); // learnphp function + expect(imports.resolved.get("MyClass")).toBeDefined(); // learnphp class + expect(imports.resolved.get("wheels")).toBeDefined(); // leanrphp variable + }); + + test("resolves include directives", () => { + const imports = resolver.resolveImports( + registree.registry.files.get(INCLUDE)!, + ); + expect(imports.unresolved.paths.length).toBe(1); + expect(imports.unresolved.paths).toContainEqual("unresolved.php"); + expect(imports.resolved.get("f")).toBeDefined(); // nested.php + expect(imports.resolved.get("defined_in_used_file")).toBeDefined(); // use.php + expect(imports.resolved.get("my_function")).toBeDefined(); // learnphp function + expect(imports.resolved.get("MyClass")).toBeDefined(); // learnphp class + expect(imports.resolved.get("wheels")).toBeDefined(); // leanrphp variable + }); +}); diff --git a/src/languagePlugins/php/incluseResolver/index.ts b/src/languagePlugins/php/incluseResolver/index.ts index e318c17b..c42e836e 100644 --- a/src/languagePlugins/php/incluseResolver/index.ts +++ b/src/languagePlugins/php/incluseResolver/index.ts @@ -1,13 +1,9 @@ import type { PHPRegistree } from "../registree/index.ts"; import { type PHPFile, type PHPNode, SymbolNode } from "../registree/types.ts"; import type { PHPImports } from "./types.ts"; -import { - PHP_INCLUDE_QUERY, - PHP_USE_DETECTION_QUERY, - PHP_USE_QUERY, -} from "./queries.ts"; +import { PHP_INCLUDE_QUERY, PHP_USE_DETECTION_QUERY } from "./queries.ts"; import type Parser from "tree-sitter"; -import { dirname } from "@std/path"; +import { dirname, join } from "@std/path"; export class PHPIncluseResolver { registree: PHPRegistree; @@ -57,19 +53,17 @@ export class PHPIncluseResolver { }, }; for (const use of useDirectives) { - const useClause = PHP_USE_QUERY.captures(use.node); - if (!useClause) continue; let name: string | undefined = undefined; let node: PHPNode | undefined = undefined; - for (const clause of useClause) { - if (clause.name === "alias") { - name = clause.node.text; + for (const clause of use.node.namedChildren) { + if (clause.type === "namespace_aliasing_clause") { + name = clause.text; } else { - node = this.registree.tree.findNode(clause.node.text); + node = this.registree.tree.findNode(clause.text); if (node && !name) { name = node.name; } else if (!node && !name) { - name = clause.node.text; + name = clause.text; } } } @@ -136,11 +130,11 @@ export class PHPIncluseResolver { imports.unresolved.paths.push(path); } } else if (include.name === "includebin") { - const filepath = this.#splitBinary(include.node) + const fileparts = this.#splitBinary(include.node) .map((n) => n.text === "__DIR__" ? dirname(file.path) : n.text) .filter((n) => n !== "") - .map((n) => n.replace(/['"]/g, "")) - .join("/"); + .map((n) => n.replace(/['"]/g, "")); + const filepath = join(fileparts[0], ...fileparts.slice(1)); const importedfile = this.registree.registry.getFile( filepath, file.path, diff --git a/src/languagePlugins/php/incluseResolver/queries.ts b/src/languagePlugins/php/incluseResolver/queries.ts index fc0b79e8..76ccb0fd 100644 --- a/src/languagePlugins/php/incluseResolver/queries.ts +++ b/src/languagePlugins/php/incluseResolver/queries.ts @@ -2,19 +2,11 @@ import Parser from "tree-sitter"; import { phpParser } from "../../../helpers/treeSitter/parsers.ts"; export const PHP_USE_DETECTION_QUERY = new Parser.Query( - phpParser.getLanguage(), - ` - (namespace_use_declaration) - `, -); - -export const PHP_USE_QUERY = new Parser.Query( phpParser.getLanguage(), ` (namespace_use_declaration - (namespace_use_clause - . (_) @ns (namespace_aliasing_clause (_) @alias)? - )) + (namespace_use_clause) @use + ) `, ); diff --git a/src/languagePlugins/php/registree/types.ts b/src/languagePlugins/php/registree/types.ts index 261b2c46..31ce9e05 100644 --- a/src/languagePlugins/php/registree/types.ts +++ b/src/languagePlugins/php/registree/types.ts @@ -44,18 +44,26 @@ export class PHPTree extends NamespaceNode { } findNode(name: string): PHPNode | undefined { - const parts = name.split("\\"); - let current: Map = this.children; - for (const part of parts) { + if (name === "") { + return this; + } else { + const packageparts = name.split("\\").reverse(); + let part = packageparts.pop()!; if (part === "") { - continue; + part = packageparts.pop()!; } - if (!current.has(part)) { + let current = this.children.get(part); + if (!current) { return undefined; } - current = current.get(part)!.children; + while (packageparts.length > 0) { + if (!current) { + return undefined; + } + current = current.children.get(packageparts.pop()!); + } + return current; } - return current.get(parts[parts.length - 1]); } addNamespaces(namespaces: ExportedNamespace[]) { diff --git a/src/languagePlugins/php/testFiles/constants.ts b/src/languagePlugins/php/testFiles/constants.ts index f39f3220..7690e1b6 100644 --- a/src/languagePlugins/php/testFiles/constants.ts +++ b/src/languagePlugins/php/testFiles/constants.ts @@ -3,3 +3,5 @@ import { join } from "@std/path"; export const LEARN_PHP = join(phpFilesFolder, "learnphp.php"); export const NESTED = join(phpFilesFolder, "nested.php"); +export const INCLUDE = join(phpFilesFolder, "include.php"); +export const USE = join(phpFilesFolder, "use.php"); diff --git a/src/languagePlugins/php/testFiles/phpFiles/imports.php b/src/languagePlugins/php/testFiles/phpFiles/imports.php deleted file mode 100644 index 56ed68a1..00000000 --- a/src/languagePlugins/php/testFiles/phpFiles/imports.php +++ /dev/null @@ -1,8 +0,0 @@ -