Skip to content

Commit 774149f

Browse files
authored
Fix/c manifest and extraction (#139)
* Fixed definesToKeep can have undefined * Gave signature/definition linking responsibility to includeResolver * Removed leftover logging * Added caching to include resolution * Changed internal inclusion structure * Changed structure for included symbols * Switched invocations to IncludedSymbol * Added first-level include recursion management * Removed includes for deleted files * Includedirs and compaction * Extraction refinement * Fixed bad handling of typedefs with same name * Improved recursive inclusion resolution * Added credits
1 parent 8a2d9ae commit 774149f

File tree

20 files changed

+563
-109
lines changed

20 files changed

+563
-109
lines changed

packages/cli/src/config/localConfig.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export const localConfigSchema = z.object({
2525
.optional(),
2626
})
2727
.optional(), // python specific config
28+
[cLanguage]: z
29+
.object({
30+
includedirs: z.array(z.string()).optional(),
31+
})
32+
.optional(), // c specific config
2833
project: z.object({
2934
include: z.array(z.string()),
3035
exclude: z.array(z.string()).optional(),

packages/cli/src/languagePlugins/c/dependencyFormatting/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ export class CDependencyFormatter {
2525

2626
constructor(
2727
files: Map<string, { path: string; rootNode: Parser.SyntaxNode }>,
28+
includeDirs: string[] = [],
2829
) {
2930
this.symbolRegistry = new CSymbolRegistry(files);
3031
this.#registry = this.symbolRegistry.getRegistry();
31-
this.includeResolver = new CIncludeResolver(this.symbolRegistry);
32+
this.includeResolver = new CIncludeResolver(
33+
this.symbolRegistry,
34+
includeDirs,
35+
);
3236
this.invocationResolver = new CInvocationResolver(this.includeResolver);
3337
}
3438

@@ -60,7 +64,7 @@ export class CDependencyFormatter {
6064
const dependencies: Record<string, CDependency> = {};
6165
const resolved = fileDependencies.resolved;
6266
for (const [symName, symbol] of resolved) {
63-
const filepath = symbol.declaration.filepath;
67+
const filepath = symbol.symbol.declaration.filepath;
6468
const id = symName;
6569
if (!dependencies[filepath]) {
6670
dependencies[filepath] = {

packages/cli/src/languagePlugins/c/extractor/index.test.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { describe, test } from "@std/testing/bdd";
22
import { expect } from "@std/expect";
3-
import { cFilesFolder, getCFilesContentMap } from "../testFiles/index.ts";
3+
import {
4+
cFilesFolder,
5+
dummyLocalConfig,
6+
getCFilesContentMap,
7+
} from "../testFiles/index.ts";
48
import { CExtractor } from "./index.ts";
59
import { join } from "@std/path";
610
import { generateCDependencyManifest } from "../../../manifest/dependencyManifest/c/index.ts";
711

812
describe("CExtractor", () => {
913
const cContentMap = getCFilesContentMap();
10-
const manifest = generateCDependencyManifest(cContentMap);
11-
const extractor = new CExtractor(cContentMap, manifest);
14+
const manifest = generateCDependencyManifest(cContentMap, dummyLocalConfig);
15+
const extractor = new CExtractor(cContentMap, manifest, dummyLocalConfig);
1216
const burgers = join(cFilesFolder, "burgers.h");
1317
const burgersc = join(cFilesFolder, "burgers.c");
18+
const main = join(cFilesFolder, "main.c");
19+
const all = join(cFilesFolder, "all.h");
20+
const errorsh = join(cFilesFolder, "errors.h");
1421
test("extracts create_burger", () => {
1522
const symbolsToExtract = new Map<
1623
string,
@@ -22,7 +29,10 @@ describe("CExtractor", () => {
2229
});
2330
const extractedFiles = extractor.extractSymbols(symbolsToExtract);
2431
expect(extractedFiles.size).toBe(2);
25-
const newManifest = generateCDependencyManifest(extractedFiles);
32+
const newManifest = generateCDependencyManifest(
33+
extractedFiles,
34+
dummyLocalConfig,
35+
);
2636
expect(newManifest[burgers]).toBeDefined();
2737
expect(newManifest[burgersc]).toBeDefined();
2838
// Expected symbols to be kept
@@ -58,7 +68,10 @@ describe("CExtractor", () => {
5868
});
5969
const extractedFiles = extractor.extractSymbols(symbolsToExtract);
6070
expect(extractedFiles.size).toBe(1);
61-
const newManifest = generateCDependencyManifest(extractedFiles);
71+
const newManifest = generateCDependencyManifest(
72+
extractedFiles,
73+
dummyLocalConfig,
74+
);
6275
expect(newManifest[burgers]).toBeDefined();
6376
// Expected symbols to be kept
6477
expect(newManifest[burgers].symbols["Drink_t"]).toBeDefined();
@@ -67,4 +80,41 @@ describe("CExtractor", () => {
6780
// Expected symbols to be removed
6881
expect(Object.keys(newManifest[burgers].symbols).length).toBe(3);
6982
});
83+
84+
test("keeps all.h", () => {
85+
const symbolsToExtract = new Map<
86+
string,
87+
{ filePath: string; symbols: Set<string> }
88+
>();
89+
symbolsToExtract.set(main, {
90+
filePath: main,
91+
symbols: new Set(["main"]),
92+
});
93+
const extractedFiles = extractor.extractSymbols(symbolsToExtract);
94+
expect(extractedFiles.size).toBe(6);
95+
const newManifest = generateCDependencyManifest(
96+
extractedFiles,
97+
dummyLocalConfig,
98+
);
99+
expect(newManifest[all]).toBeDefined();
100+
});
101+
102+
test("deletes impossible include", () => {
103+
const symbolsToExtract = new Map<
104+
string,
105+
{ filePath: string; symbols: Set<string> }
106+
>();
107+
symbolsToExtract.set(errorsh, {
108+
filePath: errorsh,
109+
symbols: new Set(["typedef"]),
110+
});
111+
const extractedFiles = extractor.extractSymbols(symbolsToExtract);
112+
expect(extractedFiles.size).toBe(1);
113+
expect(extractedFiles.get(errorsh)).toBeDefined();
114+
expect(
115+
extractedFiles.get(errorsh)?.content.includes(
116+
`#include "thisfiledoesnotexist.h"`,
117+
),
118+
).toBe(false);
119+
});
70120
});

packages/cli/src/languagePlugins/c/extractor/index.ts

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ import type { CFile, Symbol } from "../symbolRegistry/types.ts";
77
import type { ExportedFile } from "./types.ts";
88
import { C_DECLARATION_QUERY } from "../headerResolver/queries.ts";
99
import { C_IFDEF_QUERY } from "./queries.ts";
10+
import { CInvocationResolver } from "../invocationResolver/index.ts";
11+
import type z from "npm:zod";
12+
import type { localConfigSchema } from "../../../config/localConfig.ts";
13+
import { join } from "@std/path";
1014

1115
export class CExtractor {
1216
manifest: DependencyManifest;
1317
registry: Map<string, CFile>;
1418
includeResolver: CIncludeResolver;
19+
invocationResolver: CInvocationResolver;
1520

1621
constructor(
1722
files: Map<string, { path: string; content: string }>,
1823
manifest: DependencyManifest,
24+
napiConfig: z.infer<typeof localConfigSchema>,
1925
) {
2026
this.manifest = manifest;
2127
const parsedFiles = new Map<
@@ -30,7 +36,13 @@ export class CExtractor {
3036
}
3137
const symbolRegistry = new CSymbolRegistry(parsedFiles);
3238
this.registry = symbolRegistry.getRegistry();
33-
this.includeResolver = new CIncludeResolver(symbolRegistry);
39+
const outDir = napiConfig.outDir;
40+
const includeDirs = napiConfig["c"]?.includedirs ?? [];
41+
if (outDir) {
42+
includeDirs.push(...includeDirs.map((i) => join(outDir, i)));
43+
}
44+
this.includeResolver = new CIncludeResolver(symbolRegistry, includeDirs);
45+
this.invocationResolver = new CInvocationResolver(this.includeResolver);
3446
}
3547

3648
/**
@@ -100,7 +112,8 @@ export class CExtractor {
100112
const ifdefs = C_IFDEF_QUERY.captures(originalFile.rootNode).map((n) =>
101113
n.node.text
102114
);
103-
const definesToKeep = ifdefs.map((i) => fileInRegistry.symbols.get(i)!);
115+
const definesToKeep = ifdefs.map((i) => fileInRegistry.symbols.get(i)!)
116+
.filter((i) => i);
104117
const symbols = new Map<string, Symbol>();
105118
for (const define of definesToKeep) {
106119
symbols.set(define.name, define);
@@ -116,6 +129,36 @@ export class CExtractor {
116129
if (!exportedFile.symbols.has(symbolName)) {
117130
exportedFile.symbols.set(symbolName, symbol);
118131
}
132+
// Keep the files that recursively lead to a symbol we need
133+
const invocations = this.invocationResolver.getInvocationsForSymbol(
134+
symbol,
135+
);
136+
const filestokeep = Array.from(
137+
invocations.resolved.values().map((s) =>
138+
this.includeResolver.findInclusionChain(
139+
symbol.declaration.filepath,
140+
s.symbol,
141+
)
142+
).filter((c) => c !== undefined),
143+
).flatMap((c) => c.flatMap((f) => this.registry.get(f)!));
144+
for (const f of filestokeep) {
145+
if (!exportedFiles.has(f.file.path)) {
146+
const ifdefs = C_IFDEF_QUERY.captures(f.file.rootNode).map((n) =>
147+
n.node.text
148+
);
149+
const definesToKeep = ifdefs.map((i) => f.symbols.get(i)!)
150+
.filter((i) => i);
151+
const symbols = new Map<string, Symbol>();
152+
for (const define of definesToKeep) {
153+
symbols.set(define.name, define);
154+
}
155+
exportedFiles.set(f.file.path, {
156+
symbols,
157+
originalFile: f.file,
158+
strippedFile: f.file,
159+
});
160+
}
161+
}
119162
}
120163
return exportedFiles;
121164
}
@@ -127,30 +170,100 @@ export class CExtractor {
127170
#stripFiles(files: Map<string, ExportedFile>) {
128171
for (const [, file] of files) {
129172
const rootNode = file.originalFile.rootNode;
130-
const matches = C_DECLARATION_QUERY.captures(rootNode);
173+
const originalText = rootNode.text; // Original file content
131174
const symbolsToKeep = new Set(
132175
file.symbols.values().map((s) => s.declaration.node),
133176
);
134177
const symbolsToRemove = new Set<Parser.SyntaxNode>();
178+
const matches = C_DECLARATION_QUERY.captures(rootNode);
179+
135180
for (const match of matches) {
136181
const symbolNode = match.node;
137182
if (!symbolsToKeep.has(symbolNode)) {
138183
symbolsToRemove.add(symbolNode);
139184
}
140185
}
141-
let filetext = rootNode.text;
142-
for (const symbolNode of symbolsToRemove) {
143-
const symbolText = symbolNode.text;
144-
filetext = filetext.replace(symbolText, "");
145-
}
146-
const strippedFile = cParser.parse(filetext);
186+
187+
// Helper function to recursively filter nodes
188+
const filterNodes = (node: Parser.SyntaxNode): string => {
189+
if (symbolsToRemove.has(node)) {
190+
return ""; // Skip this node
191+
}
192+
193+
// If the node has children, process them recursively
194+
if (["translation_unit", "preproc_ifdef"].includes(node.type)) {
195+
let result = "";
196+
let lastEndIndex = node.startIndex;
197+
198+
for (const child of node.children) {
199+
// Append the text between the last node and the current child
200+
result += originalText.slice(lastEndIndex, child.startIndex);
201+
result += filterNodes(child); // Process the child
202+
lastEndIndex = child.endIndex;
203+
}
204+
205+
// Append the text after the last child
206+
result += originalText.slice(lastEndIndex, node.endIndex);
207+
return result;
208+
}
209+
210+
// If the node has no children, return its text
211+
return originalText.slice(node.startIndex, node.endIndex);
212+
};
213+
214+
// Rebuild the file content by filtering nodes
215+
const newFileContent = filterNodes(rootNode);
216+
217+
// Compactify the file content
218+
const compactedContent = this.#compactifyFile(newFileContent);
219+
220+
// Parse the new content and update the stripped file
221+
const strippedFile = cParser.parse(compactedContent);
147222
file.strippedFile = {
148223
path: file.originalFile.path,
149224
rootNode: strippedFile.rootNode,
150225
};
151226
}
152227
}
153228

229+
#removeDeletedIncludes(
230+
files: Map<string, ExportedFile>,
231+
) {
232+
const newproject: Map<
233+
string,
234+
{ path: string; rootNode: Parser.SyntaxNode }
235+
> = new Map();
236+
for (const [key, value] of files) {
237+
newproject.set(key, value.strippedFile);
238+
}
239+
const newregistry = new CSymbolRegistry(newproject);
240+
const newincluderes = new CIncludeResolver(
241+
newregistry,
242+
this.includeResolver.includeDirs,
243+
);
244+
newincluderes.getInclusions();
245+
for (const [key, value] of files) {
246+
const unresolved = newincluderes.unresolvedDirectives.get(key);
247+
if (unresolved) {
248+
let filetext = value.strippedFile.rootNode.text;
249+
for (const path of unresolved) {
250+
filetext = filetext.replace(`#include "${path}"`, "");
251+
}
252+
filetext = this.#compactifyFile(filetext);
253+
value.strippedFile.rootNode = cParser.parse(filetext).rootNode;
254+
}
255+
}
256+
}
257+
258+
#compactifyFile(
259+
filetext: string,
260+
): string {
261+
// Remove empty lines and useless semicolons
262+
filetext = filetext.replace(/^\s*;\s*$/gm, ""); // Remove empty lines with semicolons
263+
filetext = filetext.replace(/^\s*[\r\n]+/gm, "\n"); // Remove empty lines
264+
return filetext;
265+
}
266+
154267
/**
155268
* Finds the dependencies for a map of symbols.
156269
* @param symbolsMap - A map of file paths to their corresponding symbols.
@@ -196,6 +309,7 @@ export class CExtractor {
196309
const symbolsToExtract = this.#findDependenciesForMap(symbolsMap);
197310
const filesToExport = this.#buildFileMap(symbolsToExtract);
198311
this.#stripFiles(filesToExport);
312+
this.#removeDeletedIncludes(filesToExport);
199313
const exportedFiles = new Map<string, { path: string; content: string }>();
200314
for (const [filePath, file] of filesToExport) {
201315
const content = file.strippedFile.rootNode.text;

packages/cli/src/languagePlugins/c/headerResolver/index.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe("CHeaderResolver", () => {
1010
const burgers = join(cFilesFolder, "burgers.h");
1111
const crashcases = join(cFilesFolder, "crashcases.h");
1212
const errorsh = join(cFilesFolder, "errors.h");
13+
const oldmanh = join(cFilesFolder, "oldman.h");
1314
const file = cFilesMap.get(burgers);
1415
if (!file) {
1516
throw new Error(`File not found: ${burgers}`);
@@ -298,4 +299,27 @@ describe("CHeaderResolver", () => {
298299
const symbolNames = exportedErrorSymbols.map((symbol) => symbol.name);
299300
expect(symbolNames).toContain("#NAPI_UNNAMED_ENUM_0");
300301
});
302+
303+
test("resolves typedefs with same name as associated struct", () => {
304+
const oldmanSymbols = resolver.resolveSymbols(
305+
cFilesMap.get(oldmanh)!,
306+
);
307+
expect(oldmanSymbols).toBeDefined();
308+
const oldmen = oldmanSymbols.filter((s) => s.name === "OldMan");
309+
expect(oldmen).toHaveLength(2);
310+
const oldman = oldmen.find((s) => s.type === "typedef");
311+
expect(oldman).toBeDefined();
312+
if (!oldman) {
313+
throw new Error("OldMan is undefined");
314+
}
315+
expect(oldman.type).toBe("typedef");
316+
expect(oldman.specifiers).toEqual([]);
317+
expect(oldman.qualifiers).toEqual([]);
318+
expect(oldman.node.type).toBe("type_definition");
319+
if (!oldman.identifierNode) {
320+
throw new Error("oldman.identifierNode is undefined");
321+
}
322+
expect(oldman.identifierNode.type).toBe("type_identifier");
323+
expect(oldman.filepath).toBe(oldmanh);
324+
});
301325
});

0 commit comments

Comments
 (0)