-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathvalidate.ts
More file actions
486 lines (414 loc) · 14.1 KB
/
validate.ts
File metadata and controls
486 lines (414 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/**
* Validates extracted documentation code blocks.
* Runs language-specific type/compile checks.
*/
import * as fs from "fs";
import * as path from "path";
import { execFileSync } from "child_process";
import { glob } from "glob";
const ROOT_DIR = path.resolve(import.meta.dirname, "../..");
const VALIDATION_DIR = path.join(ROOT_DIR, "docs/.validation");
interface ValidationResult {
file: string;
sourceFile: string;
sourceLine: number;
success: boolean;
errors: string[];
}
interface Manifest {
blocks: {
id: string;
sourceFile: string;
sourceLine: number;
language: string;
outputFile: string;
}[];
}
function loadManifest(): Manifest {
const manifestPath = path.join(VALIDATION_DIR, "manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error(
"❌ No manifest found. Run extraction first: npm run extract"
);
process.exit(1);
}
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
}
async function validateTypeScript(): Promise<ValidationResult[]> {
const results: ValidationResult[] = [];
const tsDir = path.join(VALIDATION_DIR, "typescript");
const manifest = loadManifest();
if (!fs.existsSync(tsDir)) {
console.log(" No TypeScript files to validate");
return results;
}
// Create a temporary tsconfig for validation
const tsconfig = {
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
strict: true,
skipLibCheck: true,
noEmit: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
resolveJsonModule: true,
types: ["node"],
paths: {
"@github/copilot-sdk": [path.join(ROOT_DIR, "nodejs/src/index.ts")],
},
},
include: ["./**/*.ts"],
};
const tsconfigPath = path.join(tsDir, "tsconfig.json");
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
try {
// Run tsc
const tscPath = path.join(ROOT_DIR, "nodejs/node_modules/.bin/tsc");
execFileSync(tscPath, ["--project", tsconfigPath], {
encoding: "utf-8",
cwd: tsDir,
});
// All files passed
const files = await glob("*.ts", { cwd: tsDir });
for (const file of files) {
if (file === "tsconfig.json") continue;
const block = manifest.blocks.find(
(b) => b.outputFile === `typescript/${file}`
);
results.push({
file: `typescript/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: true,
errors: [],
});
}
} catch (err: any) {
// Parse tsc output for errors
const output = err.stdout || err.stderr || err.message || "";
const errorLines = output.split("\n");
const fileErrors = new Map<string, string[]>();
let currentFile = "";
for (const line of errorLines) {
const match = line.match(/^(.+\.ts)\((\d+),(\d+)\): error/);
if (match) {
currentFile = match[1];
if (!fileErrors.has(currentFile)) {
fileErrors.set(currentFile, []);
}
fileErrors.get(currentFile)!.push(line);
} else if (currentFile && line.trim()) {
fileErrors.get(currentFile)?.push(line);
}
}
// Create results
const files = await glob("*.ts", { cwd: tsDir });
for (const file of files) {
if (file === "tsconfig.json") continue;
const fullPath = path.join(tsDir, file);
const block = manifest.blocks.find(
(b) => b.outputFile === `typescript/${file}`
);
const errors = fileErrors.get(fullPath) || fileErrors.get(file) || [];
results.push({
file: `typescript/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: errors.length === 0,
errors,
});
}
}
return results;
}
async function validatePython(): Promise<ValidationResult[]> {
const results: ValidationResult[] = [];
const pyDir = path.join(VALIDATION_DIR, "python");
const manifest = loadManifest();
if (!fs.existsSync(pyDir)) {
console.log(" No Python files to validate");
return results;
}
const files = await glob("*.py", { cwd: pyDir });
for (const file of files) {
const fullPath = path.join(pyDir, file);
const block = manifest.blocks.find(
(b) => b.outputFile === `python/${file}`
);
const errors: string[] = [];
// Syntax check with py_compile
try {
execFileSync("python3", ["-m", "py_compile", fullPath], {
encoding: "utf-8",
});
} catch (err: any) {
errors.push(err.stdout || err.stderr || err.message || "Syntax error");
}
// Type check with mypy (if available)
if (errors.length === 0) {
try {
execFileSync(
"python3",
["-m", "mypy", fullPath, "--ignore-missing-imports", "--no-error-summary"],
{ encoding: "utf-8" }
);
} catch (err: any) {
const output = err.stdout || err.stderr || err.message || "";
// Filter out "Success" messages and notes
const typeErrors = output
.split("\n")
.filter(
(l: string) =>
l.includes(": error:") &&
!l.includes("Cannot find implementation")
);
if (typeErrors.length > 0) {
errors.push(...typeErrors);
}
}
}
results.push({
file: `python/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: errors.length === 0,
errors,
});
}
return results;
}
async function validateGo(): Promise<ValidationResult[]> {
const results: ValidationResult[] = [];
const goDir = path.join(VALIDATION_DIR, "go");
const manifest = loadManifest();
if (!fs.existsSync(goDir)) {
console.log(" No Go files to validate");
return results;
}
// Create a go.mod for the validation directory
const goMod = `module docs-validation
go 1.21
require github.com/github/copilot-sdk/go v0.0.0
replace github.com/github/copilot-sdk/go => ${path.join(ROOT_DIR, "go")}
`;
fs.writeFileSync(path.join(goDir, "go.mod"), goMod);
// Run go mod tidy to fetch dependencies
try {
execFileSync("go", ["mod", "tidy"], {
encoding: "utf-8",
cwd: goDir,
env: { ...process.env, GO111MODULE: "on" },
});
} catch (err: any) {
// go mod tidy might fail if there are syntax errors, continue anyway
}
const files = await glob("*.go", { cwd: goDir });
// Try to compile each file individually
for (const file of files) {
const fullPath = path.join(goDir, file);
const block = manifest.blocks.find((b) => b.outputFile === `go/${file}`);
const errors: string[] = [];
try {
// Use go vet for syntax and basic checks
execFileSync("go", ["build", "-o", "/dev/null", fullPath], {
encoding: "utf-8",
cwd: goDir,
env: { ...process.env, GO111MODULE: "on" },
});
} catch (err: any) {
const output = err.stdout || err.stderr || err.message || "";
errors.push(
...output.split("\n").filter((l: string) => l.trim() && !l.startsWith("#"))
);
}
results.push({
file: `go/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: errors.length === 0,
errors,
});
}
return results;
}
async function validateCSharp(): Promise<ValidationResult[]> {
const results: ValidationResult[] = [];
const csDir = path.join(VALIDATION_DIR, "csharp");
const manifest = loadManifest();
if (!fs.existsSync(csDir)) {
console.log(" No C# files to validate");
return results;
}
// Create a minimal csproj for validation
const csproj = `<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>CS8019;CS0168;CS0219</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="${path.join(ROOT_DIR, "dotnet/src/GitHub.Copilot.SDK.csproj")}" />
</ItemGroup>
</Project>`;
fs.writeFileSync(path.join(csDir, "DocsValidation.csproj"), csproj);
const files = await glob("*.cs", { cwd: csDir });
// Compile all files together
try {
execFileSync("dotnet", ["build", path.join(csDir, "DocsValidation.csproj")], {
encoding: "utf-8",
cwd: csDir,
});
// All files passed
for (const file of files) {
const block = manifest.blocks.find(
(b) => b.outputFile === `csharp/${file}`
);
results.push({
file: `csharp/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: true,
errors: [],
});
}
} catch (err: any) {
const output = err.stdout || err.stderr || err.message || "";
// Parse errors by file
const fileErrors = new Map<string, string[]>();
for (const line of output.split("\n")) {
const match = line.match(/([^/\\]+\.cs)\((\d+),(\d+)\): error/);
if (match) {
const fileName = match[1];
if (!fileErrors.has(fileName)) {
fileErrors.set(fileName, []);
}
fileErrors.get(fileName)!.push(line);
}
}
for (const file of files) {
const block = manifest.blocks.find(
(b) => b.outputFile === `csharp/${file}`
);
const errors = fileErrors.get(file) || [];
results.push({
file: `csharp/${file}`,
sourceFile: block?.sourceFile || "unknown",
sourceLine: block?.sourceLine || 0,
success: errors.length === 0,
errors,
});
}
}
return results;
}
function printResults(results: ValidationResult[], language: string): { failed: number; passed: number; failures: ValidationResult[] } {
const failed = results.filter((r) => !r.success);
const passed = results.filter((r) => r.success);
if (failed.length === 0) {
console.log(` ✅ ${passed.length} files passed`);
return { failed: 0, passed: passed.length, failures: [] };
}
console.log(` ❌ ${failed.length} failed, ${passed.length} passed\n`);
for (const result of failed) {
console.log(` ┌─ ${result.sourceFile}:${result.sourceLine}`);
console.log(` │ Extracted to: ${result.file}`);
for (const error of result.errors.slice(0, 5)) {
console.log(` │ ${error}`);
}
if (result.errors.length > 5) {
console.log(` │ ... and ${result.errors.length - 5} more errors`);
}
console.log(` └─`);
}
return { failed: failed.length, passed: passed.length, failures: failed };
}
function writeGitHubSummary(summaryData: { language: string; passed: number; failed: number; failures: ValidationResult[] }[]) {
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
if (!summaryFile) return;
const totalPassed = summaryData.reduce((sum, d) => sum + d.passed, 0);
const totalFailed = summaryData.reduce((sum, d) => sum + d.failed, 0);
const allPassed = totalFailed === 0;
let summary = `## 📖 Documentation Validation Results\n\n`;
if (allPassed) {
summary += `✅ **All ${totalPassed} code blocks passed validation**\n\n`;
} else {
summary += `❌ **${totalFailed} failures** out of ${totalPassed + totalFailed} code blocks\n\n`;
}
summary += `| Language | Status | Passed | Failed |\n`;
summary += `|----------|--------|--------|--------|\n`;
for (const { language, passed, failed } of summaryData) {
const status = failed === 0 ? "✅" : "❌";
summary += `| ${language} | ${status} | ${passed} | ${failed} |\n`;
}
if (totalFailed > 0) {
summary += `\n### Failures\n\n`;
for (const { language, failures } of summaryData) {
if (failures.length === 0) continue;
summary += `#### ${language}\n\n`;
for (const f of failures) {
summary += `- **${f.sourceFile}:${f.sourceLine}**\n`;
summary += ` \`\`\`\n ${f.errors.slice(0, 3).join("\n ")}\n \`\`\`\n`;
}
}
}
fs.appendFileSync(summaryFile, summary);
}
async function main() {
const args = process.argv.slice(2);
const langArg = args.find((a) => a.startsWith("--lang="));
const targetLang = langArg?.split("=")[1];
console.log("🔍 Validating documentation code blocks...\n");
if (!fs.existsSync(VALIDATION_DIR)) {
console.error("❌ No extracted code found. Run extraction first:");
console.error(" npm run extract");
process.exit(1);
}
let totalFailed = 0;
const summaryData: { language: string; passed: number; failed: number; failures: ValidationResult[] }[] = [];
const validators: [string, () => Promise<ValidationResult[]>][] = [
["TypeScript", validateTypeScript],
["Python", validatePython],
["Go", validateGo],
["C#", validateCSharp],
];
for (const [name, validator] of validators) {
const langKey = name.toLowerCase().replace("#", "sharp");
if (targetLang && langKey !== targetLang) continue;
console.log(`\n${name}:`);
const results = await validator();
const { failed, passed, failures } = printResults(results, name);
totalFailed += failed;
summaryData.push({ language: name, passed, failed, failures });
}
// Write GitHub Actions summary
writeGitHubSummary(summaryData);
console.log("\n" + "─".repeat(40));
if (totalFailed > 0) {
console.log(`\n❌ Validation failed: ${totalFailed} file(s) have errors`);
console.log("\nTo fix:");
console.log(" 1. Check the error messages above");
console.log(" 2. Update the code blocks in the markdown files");
console.log(" 3. Re-run: npm run validate");
console.log("\nTo skip a code block, add before it:");
console.log(" <!-- docs-validate: skip -->");
console.log("\nTo validate a complete version while showing a snippet:");
console.log(" <!-- docs-validate: hidden -->");
console.log(" ```lang");
console.log(" // full compilable code");
console.log(" ```");
console.log(" <!-- /docs-validate: hidden -->");
console.log(" ```lang");
console.log(" // visible snippet (auto-skipped)");
console.log(" ```");
process.exit(1);
}
console.log("\n✅ All documentation code blocks are valid!");
}
main().catch((err) => {
console.error("Validation failed:", err);
process.exit(1);
});