@photostructure/fs-metadata 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +11 -6
  2. package/CLAUDE.md +160 -136
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +2 -2
  5. package/README.md +34 -84
  6. package/binding.gyp +98 -23
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +53 -22
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +5 -0
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.mjs +52 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/{C++_REVIEW_TODO.md → doc/C++_REVIEW_TODO.md} +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +505 -0
  17. package/doc/MACOS_API_REFERENCE.md +469 -0
  18. package/doc/SECURITY_AUDIT_2025.md +809 -0
  19. package/doc/SSH_RELEASE_HOWTO.md +207 -0
  20. package/doc/WINDOWS_API_REFERENCE.md +422 -0
  21. package/doc/WINDOWS_ARM64_SECURITY.md +161 -0
  22. package/doc/WINDOWS_DEBUG_GUIDE.md +96 -0
  23. package/doc/examples.md +267 -0
  24. package/doc/gotchas.md +297 -0
  25. package/doc/logo.png +0 -0
  26. package/doc/logo.svg +85 -0
  27. package/doc/macos-asan-sip-issue.md +71 -0
  28. package/doc/social.png +0 -0
  29. package/doc/social.svg +125 -0
  30. package/doc/windows-build.md +226 -0
  31. package/doc/windows-clang-tidy.md +72 -0
  32. package/doc/windows-memory-testing.md +108 -0
  33. package/doc/windows-prebuildify-arm64.md +232 -0
  34. package/jest.config.cjs +24 -0
  35. package/package.json +68 -44
  36. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  37. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  38. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  39. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  40. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  41. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  42. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  43. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  44. package/scripts/check-memory.ts +186 -0
  45. package/scripts/clang-tidy.ts +832 -0
  46. package/scripts/install.cjs +42 -0
  47. package/scripts/is-platform.mjs +1 -1
  48. package/scripts/macos-asan.sh +155 -0
  49. package/scripts/post-build.mjs +3 -3
  50. package/scripts/prebuild-linux-glibc.sh +119 -0
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +70 -0
  53. package/scripts/sanitizers-test.sh +7 -1
  54. package/scripts/{configure.mjs → setup-native.mjs} +4 -1
  55. package/src/binding.cpp +1 -1
  56. package/src/common/error_utils.h +0 -6
  57. package/src/common/volume_metadata.h +6 -0
  58. package/src/darwin/hidden.cpp +73 -25
  59. package/src/darwin/path_security.h +149 -0
  60. package/src/darwin/raii_utils.h +104 -4
  61. package/src/darwin/volume_metadata.cpp +132 -58
  62. package/src/darwin/volume_mount_points.cpp +80 -47
  63. package/src/hidden.ts +36 -13
  64. package/src/linux/gio_mount_points.cpp +17 -18
  65. package/src/linux/gio_utils.cpp +92 -37
  66. package/src/linux/gio_utils.h +11 -5
  67. package/src/linux/gio_volume_metadata.cpp +111 -48
  68. package/src/linux/volume_metadata.cpp +67 -4
  69. package/src/object.ts +1 -0
  70. package/src/options.ts +6 -0
  71. package/src/path.ts +11 -0
  72. package/src/platform.ts +25 -0
  73. package/src/remote_info.ts +5 -3
  74. package/src/stack_path.ts +8 -6
  75. package/src/string_enum.ts +1 -0
  76. package/src/test-utils/benchmark-harness.ts +192 -0
  77. package/src/test-utils/debuglog-child.ts +30 -2
  78. package/src/test-utils/debuglog-enabled-child.ts +38 -8
  79. package/src/test-utils/jest-setup.ts +14 -0
  80. package/src/test-utils/memory-test-core.ts +336 -0
  81. package/src/test-utils/memory-test-runner.ts +108 -0
  82. package/src/test-utils/platform.ts +46 -1
  83. package/src/test-utils/worker-thread-helper.cjs +157 -26
  84. package/src/types/native_bindings.ts +1 -1
  85. package/src/types/options.ts +6 -0
  86. package/src/windows/drive_status.h +133 -163
  87. package/src/windows/error_utils.h +54 -3
  88. package/src/windows/fs_meta.h +1 -1
  89. package/src/windows/hidden.cpp +60 -43
  90. package/src/windows/security_utils.h +250 -0
  91. package/src/windows/string.h +68 -11
  92. package/src/windows/system_volume.h +1 -1
  93. package/src/windows/thread_pool.h +206 -0
  94. package/src/windows/volume_metadata.cpp +11 -6
  95. package/src/windows/volume_mount_points.cpp +8 -7
  96. package/src/windows/windows_arch.h +39 -0
  97. package/scripts/check-memory.mjs +0 -123
  98. package/scripts/clang-tidy.mjs +0 -73
@@ -0,0 +1,832 @@
1
+ #!/usr/bin/env tsx
2
+ import { exec as execCallback, execSync } from "node:child_process";
3
+ import { existsSync, writeFileSync } from "node:fs";
4
+ import { cpus, platform } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+
8
+ const exec = promisify(execCallback);
9
+
10
+ // Check for environment variable to skip
11
+ if (process.env.SKIP_CLANG_TIDY) {
12
+ console.log("Skipping clang-tidy (SKIP_CLANG_TIDY is set)");
13
+ process.exit(0);
14
+ }
15
+
16
+ // Platform detection
17
+ const isWindows = platform() === "win32";
18
+ const isMacOS = platform() === "darwin";
19
+ const isLinux = platform() === "linux";
20
+
21
+ // Colors for output
22
+ const colors = {
23
+ reset: "\x1b[0m",
24
+ red: "\x1b[31m",
25
+ green: "\x1b[32m",
26
+ yellow: "\x1b[33m",
27
+ blue: "\x1b[34m",
28
+ dim: "\x1b[2m",
29
+ } as const;
30
+
31
+ // Platform-specific warnings
32
+ if (isMacOS) {
33
+ console.log(
34
+ "Note: clang-tidy on macOS with Homebrew LLVM may report false positives",
35
+ );
36
+ console.log(
37
+ "due to header path issues. Set SKIP_CLANG_TIDY=1 to skip this check.",
38
+ );
39
+ }
40
+
41
+ // Check for required tools (non-Windows only)
42
+ function checkCommand(command: string, installHint: string): boolean {
43
+ if (isWindows) return true; // Skip on Windows
44
+
45
+ try {
46
+ execSync(`which ${command}`, { stdio: "ignore" });
47
+ return true;
48
+ } catch {
49
+ console.error(`Error: '${command}' not found in PATH.`);
50
+ console.error(`To install: ${installHint}`);
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // Find clang-tidy binary
56
+ function findClangTidy(): string | null {
57
+ if (isWindows) {
58
+ // Windows-specific paths
59
+ const windowsPaths = [
60
+ "C:\\Program Files\\LLVM\\bin\\clang-tidy.exe",
61
+ "C:\\Program Files (x86)\\LLVM\\bin\\clang-tidy.exe",
62
+ // Visual Studio 2022
63
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\Llvm\\x64\\bin\\clang-tidy.exe",
64
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\VC\\Tools\\Llvm\\x64\\bin\\clang-tidy.exe",
65
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\Llvm\\x64\\bin\\clang-tidy.exe",
66
+ // Visual Studio 2019
67
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\Llvm\\bin\\clang-tidy.exe",
68
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Professional\\VC\\Tools\\Llvm\\bin\\clang-tidy.exe",
69
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Enterprise\\VC\\Tools\\Llvm\\bin\\clang-tidy.exe",
70
+ ];
71
+
72
+ for (const path of windowsPaths) {
73
+ if (existsSync(path)) {
74
+ return path;
75
+ }
76
+ }
77
+
78
+ // Try to find in PATH
79
+ try {
80
+ execSync("where clang-tidy", { stdio: "ignore" });
81
+ return "clang-tidy";
82
+ } catch {
83
+ return null;
84
+ }
85
+ } else {
86
+ // Unix-like systems
87
+ const versions = ["", "-18", "-17", "-16", "-15", "-14"];
88
+ for (const version of versions) {
89
+ try {
90
+ const result = execSync(`which clang-tidy${version}`, {
91
+ encoding: "utf8",
92
+ }).trim();
93
+ // On macOS, return the full path if it's in Homebrew
94
+ // This allows us to detect it for filtering purposes
95
+ if (
96
+ isMacOS &&
97
+ (result.includes("/opt/homebrew") ||
98
+ result.includes("/usr/local") ||
99
+ result.includes("/Cellar"))
100
+ ) {
101
+ return result;
102
+ }
103
+ return `clang-tidy${version}`;
104
+ } catch {
105
+ // Continue trying
106
+ }
107
+ }
108
+
109
+ // On macOS, check Homebrew locations explicitly
110
+ if (isMacOS) {
111
+ const brewPrefixes = ["/opt/homebrew", "/usr/local"];
112
+ for (const prefix of brewPrefixes) {
113
+ const paths = [
114
+ `${prefix}/opt/clang-tidy/bin/clang-tidy`,
115
+ `${prefix}/opt/llvm/bin/clang-tidy`,
116
+ ];
117
+
118
+ for (const path of paths) {
119
+ if (existsSync(path)) {
120
+ return path;
121
+ }
122
+ }
123
+
124
+ // Try versioned LLVM formulas
125
+ for (let v = 18; v >= 14; v--) {
126
+ const versionedPath = `${prefix}/opt/llvm@${v}/bin/clang-tidy`;
127
+ if (existsSync(versionedPath)) {
128
+ return versionedPath;
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ return "clang-tidy"; // fallback
135
+ }
136
+ }
137
+
138
+ // Generate compile_commands.json for Windows
139
+ async function generateWindowsCompileCommands(): Promise<boolean> {
140
+ console.log("Generating compile_commands.json for Windows...");
141
+
142
+ try {
143
+ execSync("npm run setup:native", { stdio: "inherit" });
144
+
145
+ const nodeVersion = process.version.slice(1);
146
+
147
+ // Try multiple possible locations for node-gyp headers
148
+ const possibleNodeGypPaths = [
149
+ `${process.env.USERPROFILE}\\.node-gyp\\${nodeVersion}`,
150
+ `${process.env.LOCALAPPDATA}\\node-gyp\\Cache\\${nodeVersion}`,
151
+ `${process.env.APPDATA}\\npm\\node_modules\\node-gyp\\cache\\${nodeVersion}`,
152
+ ];
153
+
154
+ let nodeGyp = "";
155
+ for (const path of possibleNodeGypPaths) {
156
+ if (existsSync(join(path, "include", "node", "node.h"))) {
157
+ nodeGyp = path;
158
+ console.log("Found Node.js headers at:", nodeGyp);
159
+ break;
160
+ }
161
+ }
162
+
163
+ // If not found, install them
164
+ if (!nodeGyp) {
165
+ console.log("Installing Node.js headers...");
166
+ execSync("npx node-gyp install", { stdio: "inherit" });
167
+
168
+ // Check again
169
+ for (const path of possibleNodeGypPaths) {
170
+ if (existsSync(join(path, "include", "node", "node.h"))) {
171
+ nodeGyp = path;
172
+ break;
173
+ }
174
+ }
175
+
176
+ if (!nodeGyp) {
177
+ // Fallback to default
178
+ nodeGyp = `${process.env.USERPROFILE}\\.node-gyp\\${nodeVersion}`;
179
+ }
180
+ }
181
+
182
+ // Get all Windows sources
183
+ const windowsSources: string[] = [];
184
+ if (existsSync(join("src", "windows"))) {
185
+ const entries = require("fs").readdirSync(join("src", "windows"));
186
+ for (const entry of entries) {
187
+ if (entry.endsWith(".cpp") || entry.endsWith(".h")) {
188
+ windowsSources.push(join("src", "windows", entry));
189
+ }
190
+ }
191
+ }
192
+
193
+ // Add binding.cpp
194
+ const sources = [...windowsSources, "src/binding.cpp"];
195
+
196
+ // Find MSVC include paths
197
+ const msvcPaths = [
198
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC",
199
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Professional\\VC\\Tools\\MSVC",
200
+ "C:\\Program Files\\Microsoft Visual Studio\\2022\\Enterprise\\VC\\Tools\\MSVC",
201
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\VC\\Tools\\MSVC",
202
+ "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC",
203
+ ];
204
+
205
+ let msvcInclude = "";
206
+ for (const basePath of msvcPaths) {
207
+ if (existsSync(basePath)) {
208
+ const versions = require("fs").readdirSync(basePath);
209
+ if (versions.length > 0) {
210
+ const version = versions.sort().reverse()[0];
211
+ msvcInclude = join(basePath, version, "include");
212
+ if (existsSync(msvcInclude)) {
213
+ console.log("Found MSVC includes at:", msvcInclude);
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Find Windows SDK paths
221
+ const sdkBase = "C:\\Program Files (x86)\\Windows Kits\\10\\Include";
222
+ let sdkVersion = "";
223
+ if (existsSync(sdkBase)) {
224
+ const versions = require("fs")
225
+ .readdirSync(sdkBase)
226
+ .filter((v) => v.match(/^\d+\.\d+\.\d+\.\d+$/))
227
+ .sort()
228
+ .reverse();
229
+ if (versions.length > 0) {
230
+ sdkVersion = versions[0];
231
+ console.log("Found Windows SDK version:", sdkVersion);
232
+ }
233
+ }
234
+
235
+ // Create compile commands with absolute paths
236
+ const commands = sources.map((source: string) => ({
237
+ directory: process.cwd(),
238
+ file: source,
239
+ command: [
240
+ "clang++", // Use clang++ for clang-tidy compatibility
241
+ "-c",
242
+ source,
243
+ `-I${process.cwd()}/src/windows`,
244
+ `-I${process.cwd()}/node_modules/node-addon-api`,
245
+ `-I${nodeGyp}/include/node`,
246
+ msvcInclude ? `-I${msvcInclude}` : "",
247
+ sdkVersion ? `-I${sdkBase}\\${sdkVersion}\\ucrt` : "",
248
+ sdkVersion ? `-I${sdkBase}\\${sdkVersion}\\shared` : "",
249
+ sdkVersion ? `-I${sdkBase}\\${sdkVersion}\\um` : "",
250
+ sdkVersion ? `-I${sdkBase}\\${sdkVersion}\\winrt` : "",
251
+ "-DWIN32",
252
+ "-D_WINDOWS",
253
+ "-D_WIN64",
254
+ "-D_M_X64=1",
255
+ "-D_AMD64_=1",
256
+ "-DNAPI_VERSION=8",
257
+ "-DNODE_ADDON_API_DISABLE_DEPRECATED",
258
+ "-DBUILDING_NODE_EXTENSION",
259
+ "-std=c++17",
260
+ "-fms-compatibility",
261
+ "-fms-extensions",
262
+ "-Wno-microsoft-include",
263
+ ]
264
+ .filter((arg) => arg)
265
+ .join(" "),
266
+ }));
267
+
268
+ writeFileSync("compile_commands.json", JSON.stringify(commands, null, 2));
269
+ console.log(
270
+ `Created compile_commands.json with ${commands.length} entries`,
271
+ );
272
+ return true;
273
+ } catch (error) {
274
+ console.error("Failed to generate compile_commands.json:", error);
275
+ return false;
276
+ }
277
+ }
278
+
279
+ // Generate compile_commands.json for Unix-like systems
280
+ async function generateUnixCompileCommands(): Promise<void> {
281
+ console.log("Generating compile_commands.json...");
282
+
283
+ if (
284
+ !checkCommand(
285
+ "bear",
286
+ isLinux
287
+ ? "sudo apt-get install bear"
288
+ : isMacOS
289
+ ? "brew install bear"
290
+ : "see https://github.com/rizsotto/Bear",
291
+ )
292
+ ) {
293
+ process.exit(1);
294
+ }
295
+
296
+ execSync("npm run setup:native && bear -- npm run node-gyp-rebuild", {
297
+ stdio: "inherit",
298
+ });
299
+
300
+ if (!existsSync("compile_commands.json")) {
301
+ console.error("Failed to generate compile_commands.json");
302
+ process.exit(1);
303
+ }
304
+ }
305
+
306
+ // Get source files to check
307
+ async function getSourceFiles(): Promise<string[]> {
308
+ if (isWindows) {
309
+ // Windows-specific files
310
+ const files: string[] = [];
311
+ const windowsDir = join("src", "windows");
312
+
313
+ if (existsSync(windowsDir)) {
314
+ const entries = require("fs").readdirSync(windowsDir);
315
+ for (const entry of entries) {
316
+ if (entry.endsWith(".cpp") || entry.endsWith(".h")) {
317
+ files.push(join(windowsDir, entry));
318
+ }
319
+ }
320
+ }
321
+
322
+ // Also include binding.cpp
323
+ files.push(join("src", "binding.cpp"));
324
+ return files;
325
+ } else {
326
+ // Platform-specific exclusions for Unix-like systems
327
+ let excludePattern = "";
328
+ if (isMacOS) {
329
+ excludePattern = "| grep -v -E '(windows|linux)/'";
330
+ } else if (isLinux) {
331
+ excludePattern = "| grep -v -E '(windows|darwin)/'";
332
+ } else {
333
+ excludePattern = "| grep -v -E '(windows|darwin|linux)/'";
334
+ }
335
+
336
+ const { stdout } = await exec(
337
+ `find src -name '*.cpp' -o -name '*.h' | grep -E '\\.(cpp|h)$' ${excludePattern}`,
338
+ );
339
+ return stdout
340
+ .trim()
341
+ .split("\n")
342
+ .filter((f) => f);
343
+ }
344
+ }
345
+
346
+ // Filter out known macOS Homebrew LLVM header issues
347
+ function filterMacOSHeaderErrors(output: string): {
348
+ filteredOutput: string;
349
+ errors: number;
350
+ warnings: number;
351
+ filtered: number;
352
+ } {
353
+ const lines = output.split("\n");
354
+ const filteredLines: string[] = [];
355
+ let errors = 0;
356
+ let warnings = 0;
357
+ let filtered = 0;
358
+
359
+ // Patterns for known Homebrew LLVM vs Apple clang header mismatches
360
+ const knownHeaderErrors = [
361
+ // Standard library headers not found
362
+ /'(functional|chrono|cstring|iostream|string|vector|memory|algorithm|map|set|iterator)' file not found/,
363
+ // Apple framework headers not found when using Homebrew clang-tidy
364
+ /'CoreFoundation\/CoreFoundation\.h' file not found/,
365
+ /'DiskArbitration\/DiskArbitration\.h' file not found/,
366
+ // LLVM-specific errors that don't affect actual compilation
367
+ /unknown type name '_LIBCPP_/,
368
+ /no member named '\w+' in namespace 'std'/,
369
+ /no template named '\w+' in namespace 'std'/,
370
+ ];
371
+
372
+ for (const line of lines) {
373
+ let isKnownError = false;
374
+
375
+ // Check if this is a known header error
376
+ if (line.includes(" error:")) {
377
+ for (const pattern of knownHeaderErrors) {
378
+ if (pattern.test(line)) {
379
+ isKnownError = true;
380
+ filtered++;
381
+ break;
382
+ }
383
+ }
384
+ }
385
+
386
+ if (!isKnownError) {
387
+ filteredLines.push(line);
388
+
389
+ // Count errors and warnings
390
+ if (line.includes(" error:")) {
391
+ errors++;
392
+ }
393
+ if (line.includes(" warning:")) {
394
+ warnings++;
395
+ }
396
+ }
397
+ }
398
+
399
+ return {
400
+ filteredOutput: filteredLines.join("\n"),
401
+ errors,
402
+ warnings,
403
+ filtered,
404
+ };
405
+ }
406
+
407
+ // Filter out known Windows header issues
408
+ function filterWindowsHeaderErrors(output: string): {
409
+ filteredOutput: string;
410
+ errors: number;
411
+ warnings: number;
412
+ } {
413
+ const lines = output.split("\n");
414
+ const filteredLines: string[] = [];
415
+ let errors = 0;
416
+ let warnings = 0;
417
+ let skipNextLine = false;
418
+ let inSystemHeader = false;
419
+
420
+ // Patterns for known header issues that we want to filter out
421
+ const systemHeaderPatterns = [
422
+ // System header paths - match the entire error line
423
+ /C:\\Program Files.*:\d+:\d+: (error|warning):/,
424
+ /C:\\Users\\.*\\AppData.*:\d+:\d+: (error|warning):/,
425
+ /\.node-gyp.*:\d+:\d+: (error|warning):/,
426
+ /Windows Kits.*:\d+:\d+: (error|warning):/,
427
+ ];
428
+
429
+ // Known system header error messages that appear in user files
430
+ const systemHeaderErrors = [
431
+ // File not found errors - these are the most common MSVC header issues
432
+ /'(chrono|functional|windows\.h|iostream|string|vector|memory|algorithm|map|set|unordered_map|iterator|utility|tuple|type_traits|cstddef|cstdint|exception|new|limits|stdexcept)' file not found/,
433
+ /no member named '\w+' in the global namespace/,
434
+ /no member named '\w+' in namespace 'std'/,
435
+ /no template named '\w+' in namespace 'std'/,
436
+ /no template named 'pointer_traits'/,
437
+ /unknown type name 'stream(pos|off)'/,
438
+ /no type named 'string' in namespace 'std'/,
439
+ /use of undeclared identifier '_Elem'/,
440
+ /use of undeclared identifier 'tuple'/,
441
+ /cannot initialize return object of type 'int' with an lvalue of type 'const char/,
442
+ /expected ';' after expression/,
443
+ /unknown type name 'namespace'/,
444
+ /expected unqualified-id/,
445
+ /no type named 'type' in/,
446
+ /declaration of anonymous struct must be a definition/,
447
+ ];
448
+
449
+ for (let i = 0; i < lines.length; i++) {
450
+ // eslint-disable-next-line security/detect-object-injection
451
+ const line = lines[i];
452
+
453
+ if (skipNextLine) {
454
+ skipNextLine = false;
455
+ continue;
456
+ }
457
+
458
+ // Check if this is a system header location
459
+ let isSystemHeader = false;
460
+ for (const pattern of systemHeaderPatterns) {
461
+ if (pattern.test(line)) {
462
+ isSystemHeader = true;
463
+ inSystemHeader = true;
464
+ break;
465
+ }
466
+ }
467
+
468
+ // Check if this is a known system header error in a user file
469
+ if (!isSystemHeader && line.includes(" error:")) {
470
+ for (const pattern of systemHeaderErrors) {
471
+ if (pattern.test(line)) {
472
+ isSystemHeader = true;
473
+ break;
474
+ }
475
+ }
476
+ }
477
+
478
+ // Reset inSystemHeader flag when we see a new file
479
+ if (line.match(/^[^:]+\.(cpp|h|hpp):\d+:\d+: (error|warning):/)) {
480
+ inSystemHeader = false;
481
+ for (const pattern of systemHeaderPatterns) {
482
+ if (pattern.test(line)) {
483
+ inSystemHeader = true;
484
+ break;
485
+ }
486
+ }
487
+ }
488
+
489
+ if (!isSystemHeader && !inSystemHeader) {
490
+ // Only add non-header errors to output
491
+ filteredLines.push(line);
492
+
493
+ // Count errors and warnings
494
+ if (line.includes(" error:")) {
495
+ errors++;
496
+ }
497
+ if (line.includes(" warning:")) {
498
+ warnings++;
499
+ }
500
+ }
501
+ }
502
+
503
+ return {
504
+ filteredOutput: filteredLines.join("\n"),
505
+ errors,
506
+ warnings,
507
+ };
508
+ }
509
+
510
+ // Run clang-tidy on a single file
511
+ async function runClangTidyOnFile(
512
+ clangTidy: string,
513
+ file: string,
514
+ ): Promise<{
515
+ file: string;
516
+ output: string;
517
+ errors: number;
518
+ warnings: number;
519
+ filtered?: number;
520
+ }> {
521
+ try {
522
+ let extraArgs = "";
523
+
524
+ // Platform-specific config and arguments
525
+ if (isWindows) {
526
+ // Always use src/windows/.clang-tidy for Windows
527
+ const configPath = join("src", "windows", ".clang-tidy");
528
+ if (existsSync(configPath)) {
529
+ extraArgs = `--config-file=${configPath}`;
530
+ }
531
+ } else if (isMacOS && clangTidy.includes("/opt/")) {
532
+ // macOS with Homebrew LLVM needs extra paths
533
+ // The compile_commands.json uses Apple's /usr/bin/cc, but we're running
534
+ // Homebrew's clang-tidy which needs Homebrew's C++ stdlib
535
+ const sdkPath = execSync("xcrun --show-sdk-path", {
536
+ encoding: "utf8",
537
+ }).trim();
538
+
539
+ // Extract Homebrew prefix from clang-tidy path
540
+ const brewPrefix = clangTidy.includes("/opt/homebrew")
541
+ ? "/opt/homebrew"
542
+ : "/usr/local";
543
+
544
+ extraArgs =
545
+ `--extra-arg=-nostdinc++ ` + // Ignore built-in C++ paths
546
+ `--extra-arg=-isystem${brewPrefix}/opt/llvm/include/c++/v1 ` +
547
+ `--extra-arg=-isysroot${sdkPath} ` +
548
+ `--extra-arg=-isystem${sdkPath}/usr/include ` +
549
+ `--extra-arg=-F${sdkPath}/System/Library/Frameworks`;
550
+ }
551
+
552
+ const { stdout, stderr } = await exec(
553
+ `${isWindows ? `"${clangTidy}"` : clangTidy} -p . ${extraArgs} "${file}" 2>&1`,
554
+ );
555
+ let output = stdout + stderr;
556
+
557
+ let errors = 0;
558
+ let warnings = 0;
559
+ let filteredCount = 0;
560
+
561
+ // Filter platform-specific header errors
562
+ if (isWindows) {
563
+ const filtered = filterWindowsHeaderErrors(output);
564
+ output = filtered.filteredOutput;
565
+ errors = filtered.errors;
566
+ warnings = filtered.warnings;
567
+ } else if (isMacOS && clangTidy.includes("/opt/")) {
568
+ // Filter Homebrew LLVM errors on macOS
569
+ const filtered = filterMacOSHeaderErrors(output);
570
+ output = filtered.filteredOutput;
571
+ errors = filtered.errors;
572
+ warnings = filtered.warnings;
573
+ filteredCount = filtered.filtered;
574
+ } else {
575
+ const lines = output.split("\n");
576
+ for (const line of lines) {
577
+ if (line.includes(" warning:")) warnings++;
578
+ if (line.includes(" error:")) errors++;
579
+ }
580
+ }
581
+
582
+ return { file, output, errors, warnings, filtered: filteredCount };
583
+ } catch (error: any) {
584
+ let output = error.stdout || error.stderr || error.message;
585
+ let errors = 0;
586
+ let warnings = 0;
587
+ let filteredCount = 0;
588
+
589
+ // Filter platform-specific header errors
590
+ if (isWindows) {
591
+ const filtered = filterWindowsHeaderErrors(output);
592
+ output = filtered.filteredOutput;
593
+ errors = filtered.errors;
594
+ warnings = filtered.warnings;
595
+ } else if (isMacOS && clangTidy.includes("/opt/")) {
596
+ // Filter Homebrew LLVM errors on macOS
597
+ const filtered = filterMacOSHeaderErrors(output);
598
+ output = filtered.filteredOutput;
599
+ errors = filtered.errors;
600
+ warnings = filtered.warnings;
601
+ filteredCount = filtered.filtered;
602
+ } else {
603
+ const lines = output.split("\n");
604
+ for (const line of lines) {
605
+ if (line.includes(" warning:")) warnings++;
606
+ if (line.includes(" error:")) errors++;
607
+ }
608
+ }
609
+
610
+ return { file, output, errors, warnings, filtered: filteredCount };
611
+ }
612
+ }
613
+
614
+ // Main function
615
+ async function main(): Promise<void> {
616
+ const clangTidy = findClangTidy();
617
+
618
+ if (!clangTidy) {
619
+ console.error(`${colors.red}Error: clang-tidy not found${colors.reset}`);
620
+ if (isWindows) {
621
+ console.error("\nTo install clang-tidy on Windows:");
622
+ console.error(
623
+ "1. Install LLVM: https://github.com/llvm/llvm-project/releases",
624
+ );
625
+ console.error("2. Or install Visual Studio 2019/2022 with C++ tools");
626
+ } else if (isMacOS) {
627
+ console.error("\nTo install on macOS:");
628
+ console.error(" Option 1: brew install clang-tidy");
629
+ console.error(" Option 2: brew install llvm");
630
+ } else {
631
+ console.error("\nTo install on Linux:");
632
+ console.error(" sudo apt-get install clang-tidy");
633
+ }
634
+ process.exit(1);
635
+ }
636
+
637
+ console.log(`${colors.blue}=== Running clang-tidy ===${colors.reset}`);
638
+ console.log(`${colors.dim}Using: ${clangTidy}${colors.reset}`);
639
+ console.log(`${colors.dim}Platform: ${platform()}${colors.reset}`);
640
+
641
+ // Generate or check compile_commands.json
642
+ if (!existsSync("compile_commands.json")) {
643
+ if (isWindows) {
644
+ if (!(await generateWindowsCompileCommands())) {
645
+ process.exit(1);
646
+ }
647
+ } else {
648
+ await generateUnixCompileCommands();
649
+ }
650
+ } else {
651
+ console.log("Using existing compile_commands.json");
652
+ }
653
+
654
+ // Get files to check
655
+ const files = await getSourceFiles();
656
+ if (files.length === 0) {
657
+ console.log(
658
+ `${colors.yellow}No source files found to check${colors.reset}`,
659
+ );
660
+ return;
661
+ }
662
+
663
+ console.log(
664
+ `${colors.dim}Checking ${files.length} files...${colors.reset}\n`,
665
+ );
666
+
667
+ // Run clang-tidy on files
668
+ const parallelism = isWindows ? 1 : Math.min(cpus().length, 8);
669
+ const results: Array<{
670
+ file: string;
671
+ output: string;
672
+ errors: number;
673
+ warnings: number;
674
+ }> = [];
675
+
676
+ // Process files
677
+ if (isWindows) {
678
+ // Sequential on Windows to avoid issues
679
+ for (const file of files) {
680
+ const result = await runClangTidyOnFile(clangTidy, file);
681
+ results.push(result);
682
+
683
+ // Show progress
684
+ const relPath = file.replace(
685
+ process.cwd() + (isWindows ? "\\" : "/"),
686
+ "",
687
+ );
688
+ if (result.errors > 0) {
689
+ console.log(
690
+ `${colors.red}✗${colors.reset} ${relPath} (${result.errors} errors, ${result.warnings} warnings)`,
691
+ );
692
+ // Show first few errors (already filtered on Windows)
693
+ const errorLines = result.output
694
+ .split("\n")
695
+ .filter(
696
+ (line) => line.includes(" error:") || line.includes(" warning:"),
697
+ );
698
+ errorLines
699
+ .slice(0, 5)
700
+ .forEach((line) =>
701
+ console.log(` ${colors.dim}${line}${colors.reset}`),
702
+ );
703
+ if (errorLines.length > 5) {
704
+ console.log(
705
+ ` ${colors.dim}... and ${errorLines.length - 5} more${colors.reset}`,
706
+ );
707
+ }
708
+ } else if (result.warnings > 0) {
709
+ console.log(
710
+ `${colors.yellow}⚠${colors.reset} ${relPath} (${result.warnings} warnings)`,
711
+ );
712
+ // Show warnings (already filtered on Windows)
713
+ const warningLines = result.output
714
+ .split("\n")
715
+ .filter((line) => line.includes(" warning:"));
716
+ warningLines.forEach((line) =>
717
+ console.log(` ${colors.dim}${line}${colors.reset}`),
718
+ );
719
+ } else {
720
+ console.log(`${colors.green}✓${colors.reset} ${relPath}`);
721
+ }
722
+ }
723
+ } else {
724
+ // Parallel on Unix-like systems
725
+ for (let i = 0; i < files.length; i += parallelism) {
726
+ const chunk = files.slice(i, i + parallelism);
727
+ const chunkResults = await Promise.all(
728
+ chunk.map((file) => runClangTidyOnFile(clangTidy, file)),
729
+ );
730
+ results.push(...chunkResults);
731
+
732
+ // Show progress
733
+ for (const result of chunkResults) {
734
+ const relPath = result.file.replace(process.cwd() + "/", "");
735
+ if (result.errors > 0) {
736
+ console.log(
737
+ `${colors.red}✗${colors.reset} ${relPath} (${result.errors} errors, ${result.warnings} warnings)`,
738
+ );
739
+ // Show actual errors
740
+ const errorLines = result.output
741
+ .split("\n")
742
+ .filter(
743
+ (line) => line.includes(" error:") || line.includes(" warning:"),
744
+ );
745
+ errorLines.forEach((line) =>
746
+ console.log(` ${colors.dim}${line}${colors.reset}`),
747
+ );
748
+ } else if (result.warnings > 0) {
749
+ console.log(
750
+ `${colors.yellow}⚠${colors.reset} ${relPath} (${result.warnings} warnings)`,
751
+ );
752
+ // Show warnings
753
+ const warningLines = result.output
754
+ .split("\n")
755
+ .filter((line) => line.includes(" warning:"));
756
+ warningLines.forEach((line) =>
757
+ console.log(` ${colors.dim}${line}${colors.reset}`),
758
+ );
759
+ } else {
760
+ console.log(`${colors.green}✓${colors.reset} ${relPath}`);
761
+ }
762
+ }
763
+ }
764
+ }
765
+
766
+ // Summary
767
+ const totalErrors = results.reduce((sum, r) => sum + r.errors, 0);
768
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings, 0);
769
+
770
+ console.log(`\n${colors.blue}=== Summary ===${colors.reset}`);
771
+ if (totalErrors > 0) {
772
+ console.log(`${colors.red}✗ ${totalErrors} errors found${colors.reset}`);
773
+ }
774
+ if (totalWarnings > 0) {
775
+ console.log(
776
+ `${colors.yellow}⚠ ${totalWarnings} warnings found${colors.reset}`,
777
+ );
778
+ }
779
+ if (totalErrors === 0 && totalWarnings === 0) {
780
+ console.log(`${colors.green}✓ No issues found${colors.reset}`);
781
+ }
782
+
783
+ if (isWindows) {
784
+ console.log(
785
+ `\n${colors.dim}Windows: Using src/windows/.clang-tidy with header error filtering${colors.reset}`,
786
+ );
787
+ console.log(
788
+ `${colors.dim}Note: System header errors are automatically filtered out${colors.reset}`,
789
+ );
790
+ } else if (isMacOS && clangTidy.includes("/opt/")) {
791
+ console.log(
792
+ `\n${colors.dim}macOS: Using Homebrew LLVM clang-tidy with header error filtering${colors.reset}`,
793
+ );
794
+ console.log(
795
+ `${colors.dim}Note: Standard library header errors from toolchain mismatch are filtered out${colors.reset}`,
796
+ );
797
+
798
+ // Show filtering statistics for transparency
799
+ const totalFilesChecked = files.length;
800
+ const totalFilteredErrors = results.reduce(
801
+ (sum, r) => sum + (r.filtered || 0),
802
+ 0,
803
+ );
804
+
805
+ if (totalFilteredErrors > 0) {
806
+ console.log(
807
+ `${colors.dim}Filtered ${totalFilteredErrors} known header incompatibility error(s)${colors.reset}`,
808
+ );
809
+ }
810
+
811
+ // Sanity check: If we analyzed files and found zero issues after filtering, show validation
812
+ if (totalErrors === 0 && totalWarnings === 0 && totalFilesChecked > 0) {
813
+ if (totalFilteredErrors > 0) {
814
+ console.log(
815
+ `${colors.dim}✓ Verified: ${totalFilesChecked} files analyzed cleanly (${totalFilteredErrors} known issues filtered)${colors.reset}`,
816
+ );
817
+ } else {
818
+ console.log(
819
+ `${colors.dim}✓ Verified: ${totalFilesChecked} files analyzed with no issues${colors.reset}`,
820
+ );
821
+ }
822
+ }
823
+ }
824
+
825
+ process.exit(totalErrors > 0 ? 1 : 0);
826
+ }
827
+
828
+ // Run
829
+ main().catch((err) => {
830
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
831
+ process.exit(1);
832
+ });