Skip to content

Commit fab79be

Browse files
authored
Feature/php export resolver (#163)
* Added Zed settings for LSP * Export resolver * Added tests * length test more precise * fixed php test files * Update index.test.ts
1 parent 705e4fd commit fab79be

9 files changed

Lines changed: 1156 additions & 0 deletions

File tree

.zed/settings.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Folder-specific settings
2+
//
3+
// For a full list of overridable settings, and general information on folder-specific settings,
4+
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5+
{
6+
"lsp": {
7+
"deno": {
8+
"settings": {
9+
"deno": {
10+
"enable": true
11+
}
12+
}
13+
}
14+
},
15+
"languages": {
16+
"TSX": {
17+
"language_servers": [
18+
"deno",
19+
"!typescript-language-server",
20+
"!vtsls",
21+
"!eslint"
22+
],
23+
"formatter": "language_server"
24+
}
25+
}
26+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, test } from "@std/testing/bdd";
2+
import { expect } from "@std/expect";
3+
import { getPHPFilesMap } from "../testFiles/index.ts";
4+
import { LEARN_PHP, NESTED } from "../testFiles/constants.ts";
5+
import { PHPExportResolver } from "./index.ts";
6+
import { PHP_FUNCTION, PHP_INTERFACE, PHP_VARIABLE } from "./types.ts";
7+
8+
describe("PHP Export resolver", () => {
9+
const resolver = new PHPExportResolver();
10+
const files = getPHPFilesMap();
11+
12+
test("resolves learnphp.php", () => {
13+
const namespaces = resolver.resolveFile(files.get(LEARN_PHP)!);
14+
expect(namespaces.get("My\\Namespace")).toBeDefined();
15+
const mynamespace = namespaces.get("My\\Namespace")!;
16+
expect(mynamespace.name).toBe("My\\Namespace");
17+
const symbols = mynamespace.symbols;
18+
expect(symbols.length).toBe(66);
19+
});
20+
21+
test("resolves nested.php", () => {
22+
const namespaces = resolver.resolveFile(files.get(NESTED)!);
23+
expect(namespaces.get("")).toBeUndefined();
24+
expect(namespaces.get("All")).toBeDefined();
25+
expect(namespaces.get("All\\My")).toBeDefined();
26+
expect(namespaces.get("All\\My\\Fellas")).toBeDefined();
27+
expect(namespaces.get("All")!.symbols.length).toBe(1);
28+
const a = namespaces.get("All")!.symbols[0];
29+
expect(a.filepath).toBe(NESTED);
30+
expect(a.idNode).toBeDefined();
31+
expect(a.name).toBe("a");
32+
expect(a.namespace).toBe("All");
33+
expect(a.node).toBeDefined();
34+
expect(a.type).toBe(PHP_VARIABLE);
35+
expect(namespaces.get("All\\My")!.symbols.length).toBe(1);
36+
const m = namespaces.get("All\\My")!.symbols[0];
37+
expect(m.name).toBe("m");
38+
expect(m.type).toBe(PHP_FUNCTION);
39+
expect(namespaces.get("All\\My\\Fellas")!.symbols.length).toBe(1);
40+
const f = namespaces.get("All\\My\\Fellas")!.symbols[0];
41+
expect(f.name).toBe("f");
42+
expect(f.type).toBe(PHP_INTERFACE);
43+
});
44+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type Parser from "tree-sitter";
2+
import {
3+
type ExportedNamespace,
4+
type ExportedSymbol,
5+
PHP_VARIABLE,
6+
} from "./types.ts";
7+
import { INTERESTING_NODES, PHP_IDNODE_QUERY } from "./queries.ts";
8+
9+
export class PHPExportResolver {
10+
#currentNamespace: string = "";
11+
#currentFile: string = "";
12+
13+
resolveFile(
14+
file: { path: string; rootNode: Parser.SyntaxNode },
15+
): Map<string, ExportedNamespace> {
16+
const namespaces: Map<string, ExportedNamespace> = new Map();
17+
this.#currentNamespace =
18+
file.rootNode.children.find((c) =>
19+
c.type === "namespace_definition" && !c.childForFieldName("body")
20+
)?.childForFieldName("name")?.text ?? "";
21+
this.#currentFile = file.path;
22+
const allSymbols = this.#resolveNode(file.rootNode, this.#currentNamespace);
23+
for (const symbol of allSymbols) {
24+
if (!namespaces.has(symbol.namespace)) {
25+
namespaces.set(symbol.namespace, {
26+
name: symbol.namespace,
27+
symbols: [],
28+
});
29+
}
30+
namespaces.get(symbol.namespace)!.symbols.push(symbol);
31+
}
32+
return namespaces;
33+
}
34+
35+
#resolveNode(node: Parser.SyntaxNode, nsname: string): ExportedSymbol[] {
36+
const exports: ExportedSymbol[] = [];
37+
for (const child of node.children) {
38+
if (INTERESTING_NODES.has(child.type)) {
39+
const idNode = PHP_IDNODE_QUERY.captures(child).at(0);
40+
if (!idNode) {
41+
continue; // Root out false positives for variables and constants
42+
}
43+
const symType = INTERESTING_NODES.get(child.type)!;
44+
if (
45+
symType === PHP_VARIABLE &&
46+
exports.find((e) => e.name === idNode.node.text)
47+
) {
48+
continue; // No duplicate variables
49+
}
50+
exports.push({
51+
name: idNode.node.text,
52+
type: symType,
53+
filepath: this.#currentFile,
54+
namespace: nsname,
55+
node: child,
56+
idNode: idNode.node,
57+
});
58+
}
59+
}
60+
for (
61+
const ns of node.children.filter((c) =>
62+
c.type === "namespace_definition" && c.childForFieldName("body")
63+
)
64+
) {
65+
const fullnsname = (nsname !== "" ? nsname + "\\" : "") +
66+
ns.childForFieldName("name")!.text;
67+
const nsnode = ns.childForFieldName("body")!;
68+
exports.push(...this.#resolveNode(nsnode, fullnsname));
69+
}
70+
return exports;
71+
}
72+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Parser from "tree-sitter";
2+
import { phpParser } from "../../../helpers/treeSitter/parsers.ts";
3+
import {
4+
PHP_CLASS,
5+
PHP_CONSTANT,
6+
PHP_FUNCTION,
7+
PHP_INTERFACE,
8+
PHP_TRAIT,
9+
PHP_VARIABLE,
10+
type SymbolType,
11+
} from "./types.ts";
12+
13+
export const INTERESTING_NODES = new Map<string, SymbolType>([
14+
["const_declaration", PHP_CONSTANT],
15+
["function_definition", PHP_FUNCTION],
16+
["class_declaration", PHP_CLASS],
17+
["interface_declaration", PHP_INTERFACE],
18+
["expression_statement", PHP_VARIABLE],
19+
["function_call_expression", PHP_CONSTANT],
20+
["trait_declaration", PHP_TRAIT],
21+
]);
22+
23+
export const PHP_IDNODE_QUERY = new Parser.Query(
24+
phpParser.getLanguage(),
25+
`
26+
[
27+
(expression_statement
28+
(assignment_expression
29+
left: (variable_name
30+
(name) @name)))
31+
32+
(const_declaration
33+
(const_element
34+
(name) @name ))
35+
36+
(expression_statement
37+
(reference_assignment_expression
38+
left: (variable_name
39+
(name) @name)))
40+
41+
(function_definition
42+
name: (name) @name)
43+
44+
(class_declaration
45+
name: (name) @name)
46+
47+
(trait_declaration
48+
name: (name) @name)
49+
50+
(interface_declaration
51+
name: (name) @name)
52+
53+
(namespace_definition
54+
name: (_) @name)
55+
56+
(function_call_expression
57+
arguments: (arguments
58+
. (argument (string (string_content) @name))
59+
))
60+
]
61+
`,
62+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type Parser from "tree-sitter";
2+
3+
export const PHP_VARIABLE = "variable";
4+
export const PHP_CONSTANT = "constant";
5+
export const PHP_FUNCTION = "function";
6+
export const PHP_CLASS = "class";
7+
export const PHP_INTERFACE = "interface";
8+
export const PHP_TRAIT = "trait";
9+
export type SymbolType =
10+
| typeof PHP_VARIABLE
11+
| typeof PHP_CONSTANT
12+
| typeof PHP_FUNCTION
13+
| typeof PHP_CLASS
14+
| typeof PHP_INTERFACE
15+
| typeof PHP_TRAIT;
16+
17+
export interface ExportedNamespace {
18+
name: string;
19+
symbols: ExportedSymbol[];
20+
}
21+
22+
export interface ExportedSymbol {
23+
name: string;
24+
type: SymbolType;
25+
filepath: string;
26+
namespace: string;
27+
node: Parser.SyntaxNode;
28+
idNode: Parser.SyntaxNode;
29+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { phpFilesFolder } from "./index.ts";
2+
import { join } from "@std/path";
3+
4+
export const LEARN_PHP = join(phpFilesFolder, "learnphp.php");
5+
export const NESTED = join(phpFilesFolder, "nested.php");
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as fs from "node:fs";
2+
import { extname, join } from "@std/path";
3+
import type Parser from "tree-sitter";
4+
import { phpLanguage, phpParser } from "../../../helpers/treeSitter/parsers.ts";
5+
6+
if (!import.meta.dirname) {
7+
throw new Error("import.meta.dirname is not defined");
8+
}
9+
export const phpFilesFolder = join(
10+
import.meta.dirname,
11+
"phpFiles",
12+
);
13+
const phpFilesMap = new Map<
14+
string,
15+
{ path: string; rootNode: Parser.SyntaxNode }
16+
>();
17+
18+
/**
19+
* Recursively finds all C files in the given directory and its subdirectories.
20+
* @param dir - The directory to search in.
21+
*/
22+
function findPHPFiles(dir: string) {
23+
const files = fs.readdirSync(dir);
24+
files.forEach((file) => {
25+
const fullPath = join(dir, file);
26+
const stat = fs.statSync(fullPath);
27+
if (stat.isDirectory()) {
28+
if (
29+
!fullPath.includes(".extracted/") &&
30+
!fullPath.includes("bin/") &&
31+
!fullPath.includes("obj/")
32+
) {
33+
findPHPFiles(fullPath);
34+
}
35+
} else if (
36+
extname(fullPath) === ".php"
37+
) {
38+
const content = fs.readFileSync(fullPath, "utf8");
39+
const tree = phpParser.parse(content);
40+
phpFilesMap.set(fullPath, { path: fullPath, rootNode: tree.rootNode });
41+
}
42+
});
43+
}
44+
45+
export function getPHPFilesMap(): Map<
46+
string,
47+
{ path: string; rootNode: Parser.SyntaxNode }
48+
> {
49+
findPHPFiles(phpFilesFolder);
50+
return phpFilesMap;
51+
}
52+
53+
export function getPHPFilesContentMap(): Map<
54+
string,
55+
{ path: string; content: string }
56+
> {
57+
findPHPFiles(phpFilesFolder);
58+
const contentMap = new Map<string, { path: string; content: string }>();
59+
for (const [filePath, file] of phpFilesMap) {
60+
contentMap.set(filePath, {
61+
path: file.path,
62+
content: file.rootNode.text,
63+
});
64+
}
65+
return contentMap;
66+
}
67+
68+
export const dummyLocalConfig = {
69+
language: phpLanguage,
70+
project: {
71+
include: [],
72+
exclude: [],
73+
},
74+
outDir: "./dist",
75+
};

0 commit comments

Comments
 (0)