Skip to content

Commit e84f5dd

Browse files
committed
fix: enhance WIT validation and binding generation with source path support
- Updated `generateBindingsFromWasm` to accept an optional source path for improved context during binding generation. - Modified `validateWitSyntaxDetailedFromWasm` to include source path for detailed validation. - Introduced recursive file collection for WIT files in `wasmUtils.ts` to support local imports. - Added tests for local import resolution and validation scenarios. - Updated WIT interface definitions to reflect new parameters for validation and binding functions. - Bumped dependencies in `Cargo.lock` and `Cargo.toml` for compatibility with new features. Fixes Packages split across multiple files fail Fixes #121 Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
1 parent 58a9d99 commit e84f5dd

File tree

14 files changed

+887
-376
lines changed

14 files changed

+887
-376
lines changed

package-lock.json

Lines changed: 225 additions & 236 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,30 +75,41 @@
7575
},
7676
"dependencies": {},
7777
"devDependencies": {
78-
"@bytecodealliance/jco": "1.17.1",
78+
"@bytecodealliance/jco": "1.17.5",
7979
"@eslint/css": "1.0.0",
8080
"@eslint/js": "10.0.1",
8181
"@types/node": "25.5.0",
8282
"@types/vscode": "1.99.0",
83-
"@typescript-eslint/parser": "8.57.1",
84-
"@vitest/coverage-v8": "4.1.0",
85-
"@vitest/ui": "4.1.0",
83+
"@typescript-eslint/parser": "8.57.2",
84+
"@vitest/coverage-v8": "4.1.1",
85+
"@vitest/ui": "4.1.1",
8686
"@vscode/vsce": "3.7.1",
8787
"esbuild": "0.27.4",
8888
"esbuild-node-externals": "1.20.1",
89-
"eslint": "10.0.3",
89+
"eslint": "10.1.0",
9090
"eslint-plugin-prettier": "5.5.5",
9191
"globals": "17.4.0",
9292
"npm-run-all": "4.1.5",
93-
"ovsx": "0.10.9",
93+
"ovsx": "0.10.10",
9494
"prettier": "3.8.1",
9595
"rimraf": "6.1.3",
96-
"typescript": "5.9.3",
97-
"typescript-eslint": "8.57.1",
98-
"vitest": "4.1.0",
96+
"typescript": "6.0.2",
97+
"typescript-eslint": "8.57.2",
98+
"vitest": "4.1.1",
9999
"vscode-tmgrammar-test": "0.1.3",
100100
"wit-bindgen-wasm": "file:wit-bindgen-wasm/pkg"
101101
},
102+
"overrides": {
103+
"typescript-eslint": {
104+
"typescript": "$typescript"
105+
},
106+
"@typescript-eslint/eslint-plugin": {
107+
"typescript": "$typescript"
108+
},
109+
"@typescript-eslint/parser": {
110+
"typescript": "$typescript"
111+
}
112+
},
102113
"contributes": {
103114
"colors": [
104115
{

src/extension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,12 @@ export function activate(context: vscode.ExtensionContext) {
596596

597597
const outputPath = outputUri[0].fsPath;
598598

599-
const bindingFiles = await generateBindingsFromWasm(witContent, language);
599+
const bindingFiles = await generateBindingsFromWasm(
600+
witContent,
601+
language,
602+
undefined,
603+
diagDoc?.uri.fsPath ?? targetUri.fsPath
604+
);
600605

601606
const fileEntries = Object.entries(bindingFiles);
602607
const errorFile = fileEntries.find(([filename]) => filename === "error.txt");

src/validator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class WitSyntaxValidator {
3434
public async validate(path: string, content: string): Promise<ReturnType<typeof extractErrorInfo> | null> {
3535
try {
3636
// Use the enhanced WASM-based WIT validation with detailed error reporting
37-
const validationResult = await validateWitSyntaxDetailedFromWasm(content);
37+
const validationResult = await validateWitSyntaxDetailedFromWasm(content, path);
3838

3939
if (validationResult.valid) {
4040
return null;

src/wasmUtils.ts

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,93 @@
1+
import { readdir, readFile } from "node:fs/promises";
2+
import type { Dirent } from "node:fs";
3+
import path from "node:path";
4+
15
/**
26
* Cached reference to the witValidator interface from the WASM component module.
37
* The jco-transpiled module self-initializes via top-level await,
48
* so the module is ready to use once the dynamic import resolves.
59
*/
610
let witValidatorApi: typeof import("wit-bindgen-wasm").witValidator | null = null;
711

12+
interface PreparedSourceContext {
13+
sourcePath?: string;
14+
sourceFilesJson?: string;
15+
}
16+
17+
function normalizeSourcePath(sourcePath?: string): string | undefined {
18+
const trimmedPath = sourcePath?.trim();
19+
if (!trimmedPath) {
20+
return undefined;
21+
}
22+
23+
return path.resolve(trimmedPath);
24+
}
25+
26+
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
27+
return error instanceof Error && "code" in error;
28+
}
29+
30+
async function readDirectoryEntries(directoryPath: string): Promise<Array<Dirent<string>>> {
31+
try {
32+
return await readdir(directoryPath, { encoding: "utf8", withFileTypes: true });
33+
} catch (error: unknown) {
34+
if (isErrnoException(error) && error.code === "ENOENT") {
35+
return [];
36+
}
37+
38+
throw error;
39+
}
40+
}
41+
42+
async function collectWitFilesRecursively(directoryPath: string, sourceFiles: Record<string, string>): Promise<void> {
43+
for (const entry of await readDirectoryEntries(directoryPath)) {
44+
const entryPath = path.join(directoryPath, entry.name);
45+
if (entry.isDirectory()) {
46+
await collectWitFilesRecursively(entryPath, sourceFiles);
47+
continue;
48+
}
49+
50+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".wit")) {
51+
sourceFiles[entryPath] = await readFile(entryPath, "utf8");
52+
}
53+
}
54+
}
55+
56+
async function collectWitContext(sourceDirectory: string): Promise<Record<string, string>> {
57+
const sourceFiles: Record<string, string> = {};
58+
59+
for (const entry of await readDirectoryEntries(sourceDirectory)) {
60+
const entryPath = path.join(sourceDirectory, entry.name);
61+
if (entry.isDirectory()) {
62+
if (entry.name === "deps") {
63+
await collectWitFilesRecursively(entryPath, sourceFiles);
64+
}
65+
continue;
66+
}
67+
68+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".wit")) {
69+
sourceFiles[entryPath] = await readFile(entryPath, "utf8");
70+
}
71+
}
72+
73+
return sourceFiles;
74+
}
75+
76+
async function prepareSourceContext(content: string, sourcePath?: string): Promise<PreparedSourceContext> {
77+
const normalizedSourcePath = normalizeSourcePath(sourcePath);
78+
if (!normalizedSourcePath) {
79+
return {};
80+
}
81+
82+
const sourceFiles = await collectWitContext(path.dirname(normalizedSourcePath));
83+
sourceFiles[normalizedSourcePath] = content;
84+
85+
return {
86+
sourcePath: normalizedSourcePath,
87+
sourceFilesJson: JSON.stringify(sourceFiles),
88+
};
89+
}
90+
891
/**
992
* Initialize the WASM module by dynamically importing it.
1093
* The jco-transpiled component handles WASM loading internally.
@@ -68,9 +151,10 @@ export async function isWitFileExtensionFromWasm(filename: string): Promise<bool
68151
* @param content - The WIT content to validate
69152
* @returns Promise that resolves to true if the syntax is valid
70153
*/
71-
export async function validateWitSyntaxFromWasm(content: string): Promise<boolean> {
154+
export async function validateWitSyntaxFromWasm(content: string, sourcePath?: string): Promise<boolean> {
72155
const api = await getApi();
73-
return api.validateWitSyntax(content);
156+
const preparedSource = await prepareSourceContext(content, sourcePath);
157+
return api.validateWitSyntax(content, preparedSource.sourcePath, preparedSource.sourceFilesJson);
74158
}
75159

76160
/**
@@ -122,11 +206,19 @@ export async function extractInterfacesFromWasm(content: string): Promise<string
122206
export async function generateBindingsFromWasm(
123207
content: string,
124208
language: string,
125-
worldName?: string
209+
worldName?: string,
210+
sourcePath?: string
126211
): Promise<Record<string, string>> {
127212
const api = await getApi();
128-
const jsonResult = api.generateBindings(content, language, worldName);
129-
return JSON.parse(jsonResult);
213+
const preparedSource = await prepareSourceContext(content, sourcePath);
214+
const jsonResult = api.generateBindings(
215+
content,
216+
language,
217+
worldName,
218+
preparedSource.sourcePath,
219+
preparedSource.sourceFilesJson
220+
);
221+
return JSON.parse(jsonResult) as Record<string, string>;
130222
}
131223

132224
/**
@@ -143,8 +235,16 @@ export interface WitValidationResult {
143235
* @param content - The WIT content to validate
144236
* @returns Promise that resolves to detailed validation results
145237
*/
146-
export async function validateWitSyntaxDetailedFromWasm(content: string): Promise<WitValidationResult> {
238+
export async function validateWitSyntaxDetailedFromWasm(
239+
content: string,
240+
sourcePath?: string
241+
): Promise<WitValidationResult> {
147242
const api = await getApi();
148-
const resultJson = api.validateWitSyntaxDetailed(content);
149-
return JSON.parse(resultJson);
243+
const preparedSource = await prepareSourceContext(content, sourcePath);
244+
const resultJson = api.validateWitSyntaxDetailed(
245+
content,
246+
preparedSource.sourcePath,
247+
preparedSource.sourceFilesJson
248+
);
249+
return JSON.parse(resultJson) as WitValidationResult;
150250
}

tests/bindings-generation.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("Bindings Generation for All Languages", () => {
2828

2929
supportedLanguages.forEach(({ lang, extension, expectedContent, minLength }) => {
3030
it(`should generate actual code stubs for ${lang}`, () => {
31-
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
31+
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
3232
const result = JSON.parse(resultJson);
3333

3434
// Verify files were generated
@@ -66,7 +66,7 @@ describe("Bindings Generation for All Languages", () => {
6666
});
6767

6868
it(`should not generate only README files for ${lang}`, () => {
69-
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
69+
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
7070
const result = JSON.parse(resultJson);
7171

7272
// Verify that not all files are README files
@@ -90,7 +90,7 @@ describe("Bindings Generation for All Languages", () => {
9090
const results: Record<string, Record<string, string>> = {};
9191

9292
for (const { lang } of supportedLanguages) {
93-
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined);
93+
const resultJson = witValidator.generateBindings(TEST_WIT, lang, undefined, undefined, undefined);
9494
results[lang] = JSON.parse(resultJson);
9595
}
9696

@@ -149,7 +149,7 @@ world app {
149149
`;
150150

151151
for (const { lang, extension } of supportedLanguages) {
152-
const resultJson = witValidator.generateBindings(complexWit, lang, undefined);
152+
const resultJson = witValidator.generateBindings(complexWit, lang, undefined, undefined, undefined);
153153
const result = JSON.parse(resultJson);
154154

155155
expect(Object.keys(result).length).toBeGreaterThan(0);

tests/grammar/issue-121/wit/a.wit

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package local:demo;
2+
3+
world my-world {
4+
import host;
5+
6+
export another-interface;
7+
}
8+
9+
interface host {
10+
// ...
11+
}

tests/grammar/issue-121/wit/b.wit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
interface another-interface {
2+
// ...
3+
}

tests/import-resolution.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
6+
import { generateBindingsFromWasm, validateWitSyntaxDetailedFromWasm } from "../src/wasmUtils.js";
7+
8+
describe("local import resolution", () => {
9+
const tempDirectories: string[] = [];
10+
11+
afterEach(async () => {
12+
await Promise.all(tempDirectories.map((directory) => rm(directory, { recursive: true, force: true })));
13+
tempDirectories.length = 0;
14+
});
15+
16+
async function createIssue121Fixture(): Promise<{ content: string; sourcePath: string }> {
17+
const fixtureRoot = await mkdtemp(path.join(os.tmpdir(), "wit-idl-imports-"));
18+
tempDirectories.push(fixtureRoot);
19+
20+
const witDirectory = path.join(fixtureRoot, "wit");
21+
await mkdir(witDirectory, { recursive: true });
22+
23+
const sourcePath = path.join(witDirectory, "a.wit");
24+
await writeFile(
25+
sourcePath,
26+
`package local:demo;
27+
28+
world my-world {
29+
import host;
30+
31+
export another-interface;
32+
}
33+
34+
interface host {
35+
ping: func();
36+
}
37+
`,
38+
"utf8"
39+
);
40+
await writeFile(
41+
path.join(witDirectory, "b.wit"),
42+
`interface another-interface {
43+
pong: func();
44+
}
45+
`,
46+
"utf8"
47+
);
48+
49+
return {
50+
content: await readFile(sourcePath, "utf8"),
51+
sourcePath,
52+
};
53+
}
54+
55+
it("validates WIT with sibling imports when given a source path", async () => {
56+
const fixture = await createIssue121Fixture();
57+
58+
const result = await validateWitSyntaxDetailedFromWasm(fixture.content, fixture.sourcePath);
59+
60+
expect(result).toEqual({ valid: true });
61+
});
62+
63+
it("uses unsaved editor content while still resolving sibling imports", async () => {
64+
const fixture = await createIssue121Fixture();
65+
const invalidContent = `${fixture.content}\nthis is not valid wit\n`;
66+
67+
const result = await validateWitSyntaxDetailedFromWasm(invalidContent, fixture.sourcePath);
68+
69+
expect(result.valid).toBe(false);
70+
expect(result.error).toContain(fixture.sourcePath);
71+
});
72+
73+
it("generates bindings from in-memory content while resolving sibling imports", async () => {
74+
const fixture = await createIssue121Fixture();
75+
const updatedContent = fixture.content.replace("world my-world", "world staged-world");
76+
77+
const files = await generateBindingsFromWasm(updatedContent, "rust", "staged-world", fixture.sourcePath);
78+
79+
expect(files["error.txt"]).toBeUndefined();
80+
expect(Object.keys(files).some((filename) => filename.endsWith(".rs"))).toBe(true);
81+
});
82+
});

0 commit comments

Comments
 (0)