@photostructure/fs-metadata 0.6.1 → 0.7.1

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 (89) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/CLAUDE.md +141 -315
  3. package/CODE_OF_CONDUCT.md +11 -11
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +34 -103
  6. package/binding.gyp +97 -22
  7. package/claude.sh +23 -0
  8. package/dist/index.cjs +51 -21
  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 +51 -21
  14. package/dist/index.mjs.map +1 -1
  15. package/doc/C++_REVIEW_TODO.md +97 -25
  16. package/doc/GPG_RELEASE_HOWTO.md +44 -13
  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 +28 -24
  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 +9 -2
  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 +23 -0
  35. package/package.json +61 -36
  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 +690 -99
  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 +12 -1
  51. package/scripts/prebuildify-wrapper.ts +77 -0
  52. package/scripts/precommit.ts +45 -20
  53. package/scripts/sanitizers-test.sh +1 -1
  54. package/src/common/volume_metadata.h +6 -0
  55. package/src/darwin/hidden.cpp +73 -25
  56. package/src/darwin/path_security.h +149 -0
  57. package/src/darwin/raii_utils.h +104 -4
  58. package/src/darwin/volume_metadata.cpp +132 -58
  59. package/src/darwin/volume_mount_points.cpp +80 -47
  60. package/src/hidden.ts +36 -13
  61. package/src/linux/gio_mount_points.cpp +17 -18
  62. package/src/linux/gio_utils.cpp +92 -37
  63. package/src/linux/gio_utils.h +11 -5
  64. package/src/linux/gio_volume_metadata.cpp +111 -48
  65. package/src/linux/volume_metadata.cpp +67 -4
  66. package/src/object.ts +1 -0
  67. package/src/options.ts +6 -0
  68. package/src/path.ts +11 -0
  69. package/src/remote_info.ts +5 -3
  70. package/src/stack_path.ts +8 -6
  71. package/src/string_enum.ts +1 -0
  72. package/src/test-utils/memory-test-core.ts +336 -0
  73. package/src/test-utils/memory-test-runner.ts +108 -0
  74. package/src/test-utils/platform.ts +46 -1
  75. package/src/test-utils/worker-thread-helper.cjs +154 -27
  76. package/src/types/native_bindings.ts +1 -1
  77. package/src/types/options.ts +6 -0
  78. package/src/windows/drive_status.h +133 -163
  79. package/src/windows/error_utils.h +54 -3
  80. package/src/windows/fs_meta.h +1 -1
  81. package/src/windows/hidden.cpp +60 -43
  82. package/src/windows/security_utils.h +250 -0
  83. package/src/windows/string.h +68 -11
  84. package/src/windows/system_volume.h +1 -1
  85. package/src/windows/thread_pool.h +206 -0
  86. package/src/windows/volume_metadata.cpp +11 -6
  87. package/src/windows/volume_mount_points.cpp +8 -7
  88. package/src/windows/windows_arch.h +39 -0
  89. package/scripts/check-memory.mjs +0 -123
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { exec as execCallback, execSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, writeFileSync } from "node:fs";
4
4
  import { cpus, platform } from "node:os";
5
+ import { join } from "node:path";
5
6
  import { promisify } from "node:util";
6
7
 
7
8
  const exec = promisify(execCallback);
8
9
 
9
- // Skip clang-tidy on Windows
10
- if (platform() === "win32") {
11
- console.log("Skipping clang-tidy on Windows platform");
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)");
12
13
  process.exit(0);
13
14
  }
14
15
 
16
+ // Platform detection
17
+ const isWindows = platform() === "win32";
18
+ const isMacOS = platform() === "darwin";
19
+ const isLinux = platform() === "linux";
20
+
15
21
  // Colors for output
16
22
  const colors = {
17
23
  reset: "\x1b[0m",
@@ -22,8 +28,20 @@ const colors = {
22
28
  dim: "\x1b[2m",
23
29
  } as const;
24
30
 
25
- // Check for required tools
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)
26
42
  function checkCommand(command: string, installHint: string): boolean {
43
+ if (isWindows) return true; // Skip on Windows
44
+
27
45
  try {
28
46
  execSync(`which ${command}`, { stdio: "ignore" });
29
47
  return true;
@@ -34,136 +52,606 @@ function checkCommand(command: string, installHint: string): boolean {
34
52
  }
35
53
  }
36
54
 
37
- const isMacOS = platform() === "darwin";
38
- const isLinux = platform() === "linux";
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
+ ];
39
71
 
40
- let hasAllTools = true;
41
-
42
- if (
43
- !checkCommand(
44
- "bear",
45
- isLinux
46
- ? "sudo apt-get install bear"
47
- : isMacOS
48
- ? "brew install bear"
49
- : "see https://github.com/rizsotto/Bear",
50
- )
51
- ) {
52
- hasAllTools = false;
53
- }
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
+ ];
54
117
 
55
- if (
56
- !checkCommand(
57
- "clang-tidy",
58
- isLinux
59
- ? "sudo apt-get install clang-tidy"
60
- : isMacOS
61
- ? "brew install llvm && alias clang-tidy=$(brew --prefix llvm)/bin/clang-tidy"
62
- : "see https://clang.llvm.org/extra/clang-tidy/",
63
- )
64
- ) {
65
- hasAllTools = false;
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
+ }
66
136
  }
67
137
 
68
- if (!hasAllTools) {
69
- process.exit(1);
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
+ }
70
277
  }
71
278
 
72
- // Generate compile_commands.json if needed
73
- const compileCommandsPath = "compile_commands.json";
74
- if (existsSync(compileCommandsPath)) {
75
- console.log("Using existing compile_commands.json");
76
- } else {
279
+ // Generate compile_commands.json for Unix-like systems
280
+ async function generateUnixCompileCommands(): Promise<void> {
77
281
  console.log("Generating compile_commands.json...");
78
282
 
79
- // Use bear to generate compile_commands.json
80
- // Bear intercepts the build commands and creates the compilation database
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
+
81
296
  execSync("npm run setup:native && bear -- npm run node-gyp-rebuild", {
82
297
  stdio: "inherit",
83
298
  });
84
299
 
85
- // Check if it was created successfully
86
- if (!existsSync(compileCommandsPath)) {
300
+ if (!existsSync("compile_commands.json")) {
87
301
  console.error("Failed to generate compile_commands.json");
88
- console.error("Make sure bear is installed: sudo apt-get install bear");
89
302
  process.exit(1);
90
303
  }
91
304
  }
92
305
 
93
- // Find clang-tidy binary (try different versions)
94
- function findClangTidy(): string {
95
- const versions = ["", "-18", "-17", "-16", "-15", "-14"];
96
- for (const version of versions) {
97
- try {
98
- execSync(`which clang-tidy${version}`, { stdio: "ignore" });
99
- return `clang-tidy${version}`;
100
- } catch {
101
- // Continue trying
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
+ }
102
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);
103
343
  }
104
- return "clang-tidy"; // fallback
105
344
  }
106
345
 
107
- // Get list of files to check
108
- async function getSourceFiles(): Promise<string[]> {
109
- const { stdout } = await exec(
110
- `find src -name '*.cpp' -o -name '*.h' | grep -E '\\.(cpp|h)$' | grep -v -E '(windows|darwin)/'`,
111
- );
112
- return stdout
113
- .trim()
114
- .split("\n")
115
- .filter((f) => f);
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
+ };
116
405
  }
117
406
 
118
- interface TidyResult {
119
- file: string;
120
- output: string;
407
+ // Filter out known Windows header issues
408
+ function filterWindowsHeaderErrors(output: string): {
409
+ filteredOutput: string;
121
410
  errors: number;
122
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
+ };
123
508
  }
124
509
 
125
510
  // Run clang-tidy on a single file
126
511
  async function runClangTidyOnFile(
127
512
  clangTidy: string,
128
513
  file: string,
129
- ): Promise<TidyResult> {
514
+ ): Promise<{
515
+ file: string;
516
+ output: string;
517
+ errors: number;
518
+ warnings: number;
519
+ filtered?: number;
520
+ }> {
130
521
  try {
131
- const { stdout, stderr } = await exec(`${clangTidy} -p . "${file}" 2>&1`);
132
- const output = stdout + stderr;
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;
133
556
 
134
557
  let errors = 0;
135
558
  let warnings = 0;
136
- const lines = output.split("\n");
559
+ let filteredCount = 0;
137
560
 
138
- for (const line of lines) {
139
- if (line.includes(" warning:")) warnings++;
140
- if (line.includes(" error:")) errors++;
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
+ }
141
580
  }
142
581
 
143
- return { file, output, errors, warnings };
582
+ return { file, output, errors, warnings, filtered: filteredCount };
144
583
  } catch (error: any) {
145
- // clang-tidy returns non-zero on errors, capture output
146
- const output = error.stdout || error.stderr || error.message;
584
+ let output = error.stdout || error.stderr || error.message;
147
585
  let errors = 0;
148
586
  let warnings = 0;
587
+ let filteredCount = 0;
149
588
 
150
- const lines = output.split("\n");
151
- for (const line of lines) {
152
- if (line.includes(" warning:")) warnings++;
153
- if (line.includes(" error:")) errors++;
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
+ }
154
608
  }
155
609
 
156
- return { file, output, errors, warnings };
610
+ return { file, output, errors, warnings, filtered: filteredCount };
157
611
  }
158
612
  }
159
613
 
160
614
  // Main function
161
615
  async function main(): Promise<void> {
162
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
+
163
637
  console.log(`${colors.blue}=== Running clang-tidy ===${colors.reset}`);
164
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
+ }
165
653
 
166
- // Get files
654
+ // Get files to check
167
655
  const files = await getSourceFiles();
168
656
  if (files.length === 0) {
169
657
  console.log(
@@ -176,42 +664,103 @@ async function main(): Promise<void> {
176
664
  `${colors.dim}Checking ${files.length} files...${colors.reset}\n`,
177
665
  );
178
666
 
179
- // Run clang-tidy on files in parallel
180
- const parallelism = Math.min(cpus().length, 8);
181
- const results: TidyResult[] = [];
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
+ }> = [];
182
675
 
183
- // Process files in chunks
184
- for (let i = 0; i < files.length; i += parallelism) {
185
- const chunk = files.slice(i, i + parallelism);
186
- const chunkResults = await Promise.all(
187
- chunk.map((file) => runClangTidyOnFile(clangTidy, file)),
188
- );
189
- results.push(...chunkResults);
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);
190
682
 
191
- // Show progress
192
- for (const result of chunkResults) {
193
- const relPath = result.file.replace(process.cwd() + "/", "");
683
+ // Show progress
684
+ const relPath = file.replace(
685
+ process.cwd() + (isWindows ? "\\" : "/"),
686
+ "",
687
+ );
194
688
  if (result.errors > 0) {
195
689
  console.log(
196
690
  `${colors.red}✗${colors.reset} ${relPath} (${result.errors} errors, ${result.warnings} warnings)`,
197
691
  );
198
- // Show actual errors
692
+ // Show first few errors (already filtered on Windows)
199
693
  const errorLines = result.output
200
694
  .split("\n")
201
695
  .filter(
202
696
  (line) => line.includes(" error:") || line.includes(" warning:"),
203
697
  );
204
- errorLines.forEach((line) =>
205
- console.log(` ${colors.dim}${line}${colors.reset}`),
206
- );
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
+ }
207
708
  } else if (result.warnings > 0) {
208
709
  console.log(
209
710
  `${colors.yellow}⚠${colors.reset} ${relPath} (${result.warnings} warnings)`,
210
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
+ );
211
719
  } else {
212
720
  console.log(`${colors.green}✓${colors.reset} ${relPath}`);
213
721
  }
214
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
+ }
215
764
  }
216
765
 
217
766
  // Summary
@@ -231,6 +780,48 @@ async function main(): Promise<void> {
231
780
  console.log(`${colors.green}✓ No issues found${colors.reset}`);
232
781
  }
233
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
+
234
825
  process.exit(totalErrors > 0 ? 1 : 0);
235
826
  }
236
827