@openrewrite/rewrite 8.70.0-20251219-180817 → 8.70.0-20251219-215811

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 (94) hide show
  1. package/dist/cli/cli-utils.d.ts +6 -6
  2. package/dist/cli/cli-utils.d.ts.map +1 -1
  3. package/dist/cli/cli-utils.js +50 -228
  4. package/dist/cli/cli-utils.js.map +1 -1
  5. package/dist/javascript/assertions.d.ts.map +1 -1
  6. package/dist/javascript/assertions.js +87 -12
  7. package/dist/javascript/assertions.js.map +1 -1
  8. package/dist/javascript/autodetect.d.ts +11 -11
  9. package/dist/javascript/autodetect.d.ts.map +1 -1
  10. package/dist/javascript/autodetect.js +18 -21
  11. package/dist/javascript/autodetect.js.map +1 -1
  12. package/dist/javascript/format/prettier-config-loader.d.ts.map +1 -1
  13. package/dist/javascript/format/prettier-config-loader.js +1 -1
  14. package/dist/javascript/format/prettier-config-loader.js.map +1 -1
  15. package/dist/javascript/index.d.ts +1 -0
  16. package/dist/javascript/index.d.ts.map +1 -1
  17. package/dist/javascript/index.js +1 -0
  18. package/dist/javascript/index.js.map +1 -1
  19. package/dist/javascript/markers.d.ts.map +1 -1
  20. package/dist/javascript/markers.js +135 -6
  21. package/dist/javascript/markers.js.map +1 -1
  22. package/dist/javascript/node-resolution-result.d.ts +4 -1
  23. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  24. package/dist/javascript/node-resolution-result.js +22 -1
  25. package/dist/javascript/node-resolution-result.js.map +1 -1
  26. package/dist/javascript/package-json-parser.d.ts +7 -0
  27. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  28. package/dist/javascript/package-json-parser.js +19 -1
  29. package/dist/javascript/package-json-parser.js.map +1 -1
  30. package/dist/javascript/parser.d.ts.map +1 -1
  31. package/dist/javascript/parser.js +1 -13
  32. package/dist/javascript/parser.js.map +1 -1
  33. package/dist/javascript/preconditions.js +4 -4
  34. package/dist/javascript/preconditions.js.map +1 -1
  35. package/dist/javascript/project-parser.d.ts +137 -0
  36. package/dist/javascript/project-parser.d.ts.map +1 -0
  37. package/dist/javascript/project-parser.js +726 -0
  38. package/dist/javascript/project-parser.js.map +1 -0
  39. package/dist/javascript/style.d.ts +9 -26
  40. package/dist/javascript/style.d.ts.map +1 -1
  41. package/dist/javascript/style.js +18 -42
  42. package/dist/javascript/style.js.map +1 -1
  43. package/dist/json/parser.d.ts.map +1 -1
  44. package/dist/json/parser.js +1 -0
  45. package/dist/json/parser.js.map +1 -1
  46. package/dist/markers.d.ts +1 -1
  47. package/dist/markers.js +1 -1
  48. package/dist/markers.js.map +1 -1
  49. package/dist/parser.d.ts +1 -1
  50. package/dist/parser.d.ts.map +1 -1
  51. package/dist/rpc/index.d.ts +0 -1
  52. package/dist/rpc/index.d.ts.map +1 -1
  53. package/dist/rpc/index.js +4 -2
  54. package/dist/rpc/index.js.map +1 -1
  55. package/dist/rpc/request/index.d.ts +1 -0
  56. package/dist/rpc/request/index.d.ts.map +1 -1
  57. package/dist/rpc/request/index.js +1 -0
  58. package/dist/rpc/request/index.js.map +1 -1
  59. package/dist/rpc/request/parse-project.d.ts +25 -0
  60. package/dist/rpc/request/parse-project.d.ts.map +1 -0
  61. package/dist/rpc/request/parse-project.js +304 -0
  62. package/dist/rpc/request/parse-project.js.map +1 -0
  63. package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
  64. package/dist/rpc/rewrite-rpc.js +1 -0
  65. package/dist/rpc/rewrite-rpc.js.map +1 -1
  66. package/dist/text/parser.d.ts.map +1 -1
  67. package/dist/text/parser.js +1 -0
  68. package/dist/text/parser.js.map +1 -1
  69. package/dist/version.txt +1 -1
  70. package/dist/yaml/parser.d.ts.map +1 -1
  71. package/dist/yaml/parser.js +52 -4
  72. package/dist/yaml/parser.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/cli/cli-utils.ts +46 -237
  75. package/src/javascript/assertions.ts +74 -10
  76. package/src/javascript/autodetect.ts +22 -15
  77. package/src/javascript/format/prettier-config-loader.ts +2 -2
  78. package/src/javascript/index.ts +1 -0
  79. package/src/javascript/markers.ts +157 -7
  80. package/src/javascript/node-resolution-result.ts +23 -2
  81. package/src/javascript/package-json-parser.ts +19 -1
  82. package/src/javascript/parser.ts +1 -16
  83. package/src/javascript/preconditions.ts +1 -1
  84. package/src/javascript/project-parser.ts +657 -0
  85. package/src/javascript/style.ts +43 -28
  86. package/src/json/parser.ts +3 -1
  87. package/src/markers.ts +1 -1
  88. package/src/parser.ts +1 -1
  89. package/src/rpc/index.ts +7 -5
  90. package/src/rpc/request/index.ts +1 -0
  91. package/src/rpc/request/parse-project.ts +283 -0
  92. package/src/rpc/rewrite-rpc.ts +2 -0
  93. package/src/text/parser.ts +3 -1
  94. package/src/yaml/parser.ts +53 -5
@@ -0,0 +1,657 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import * as path from "path";
17
+ import * as fs from "fs";
18
+ import * as fsp from "fs/promises";
19
+ import {spawnSync} from "child_process";
20
+ import picomatch from "picomatch";
21
+ import {produce} from "immer";
22
+ import {SourceFile} from "../tree";
23
+ import {Parsers} from "../parser";
24
+ import {PrettierConfigLoader} from "./format/prettier-config-loader";
25
+ import {ExecutionContext} from "../execution";
26
+ import {Marker} from "../markers";
27
+
28
+ // Lock file names defined here to avoid circular dependency with package-manager.ts
29
+ // These must be kept in sync with the definitions in package-manager.ts
30
+ const JSON_LOCK_FILE_NAMES = ['bun.lock', 'package-lock.json'] as const;
31
+ const YAML_LOCK_FILE_NAMES = ['pnpm-lock.yaml'] as const;
32
+ const TEXT_LOCK_FILE_NAMES = ['yarn.lock'] as const;
33
+
34
+ /**
35
+ * Detects if a yarn.lock file is Yarn Berry (v2+) format based on content.
36
+ * Yarn Berry lock files contain a `__metadata:` key which is not present in Classic.
37
+ */
38
+ function isYarnBerryLockFile(content: string): boolean {
39
+ return content.includes('__metadata:');
40
+ }
41
+
42
+ /**
43
+ * Options for project parsing.
44
+ */
45
+ export interface ProjectParserOptions {
46
+ /**
47
+ * Optional execution context.
48
+ */
49
+ ctx?: ExecutionContext;
50
+
51
+ /**
52
+ * Glob patterns to exclude from source file discovery.
53
+ * Default excludes: node_modules, dist, build, .git, coverage, *.min.js, *.bundle.js
54
+ */
55
+ exclusions?: string[];
56
+
57
+ /**
58
+ * Whether to use git to discover files (respects .gitignore).
59
+ * Default: true if in a git repository.
60
+ */
61
+ useGit?: boolean;
62
+
63
+ /**
64
+ * Progress callback for file parsing.
65
+ */
66
+ onProgress?: (phase: "discovering" | "parsing", current: number, total: number, filePath?: string) => void;
67
+
68
+ /**
69
+ * Whether to enable verbose logging.
70
+ */
71
+ verbose?: boolean;
72
+
73
+ /**
74
+ * Optional predicate to filter which files should be parsed.
75
+ * Called with the absolute file path after file discovery.
76
+ * Return true to include the file, false to exclude it.
77
+ * If not provided, all discovered files are parsed.
78
+ */
79
+ fileFilter?: (absolutePath: string) => boolean;
80
+ }
81
+
82
+ /**
83
+ * Result of file discovery.
84
+ */
85
+ export interface DiscoveredFiles {
86
+ /** package.json files - parsed with PackageJsonParser for NodeResolutionResult */
87
+ packageJsonFiles: string[];
88
+ /** Lock files grouped by parser type */
89
+ lockFiles: {
90
+ json: string[]; // package-lock.json, bun.lock
91
+ yaml: string[]; // pnpm-lock.yaml, yarn.lock (Berry)
92
+ text: string[]; // yarn.lock (Classic)
93
+ };
94
+ /** JavaScript/TypeScript files (includes .prettierrc.js, prettier.config.js) */
95
+ jsFiles: string[];
96
+ /** JSON files (tsconfig.json, .prettierrc.json, other .json) */
97
+ jsonFiles: string[];
98
+ /** YAML files (.prettierrc.yaml, other .yaml/.yml) */
99
+ yamlFiles: string[];
100
+ /** Plain text config files (.prettierignore, .gitignore, etc.) */
101
+ textFiles: string[];
102
+ }
103
+
104
+ /**
105
+ * Default exclusion patterns for file discovery.
106
+ */
107
+ export const DEFAULT_EXCLUSIONS = [
108
+ "**/node_modules/**",
109
+ "**/dist/**",
110
+ "**/build/**",
111
+ "**/.git/**",
112
+ "**/coverage/**",
113
+ "**/*.min.js",
114
+ "**/*.bundle.js"
115
+ ];
116
+
117
+ /**
118
+ * Source file extensions for JavaScript/TypeScript.
119
+ */
120
+ const SOURCE_EXTENSIONS = new Set([
121
+ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"
122
+ ]);
123
+
124
+ /**
125
+ * All lock file names for quick lookup.
126
+ */
127
+ const ALL_LOCK_FILE_NAMES = new Set<string>([
128
+ ...JSON_LOCK_FILE_NAMES,
129
+ ...YAML_LOCK_FILE_NAMES,
130
+ ...TEXT_LOCK_FILE_NAMES
131
+ ]);
132
+
133
+ /**
134
+ * Plain text config files (no extension).
135
+ */
136
+ const TEXT_CONFIG_FILES = new Set([
137
+ ".prettierignore",
138
+ ".gitignore",
139
+ ".npmignore",
140
+ ".eslintignore"
141
+ ]);
142
+
143
+ /**
144
+ * Parses an entire JavaScript/TypeScript project.
145
+ *
146
+ * This class handles:
147
+ * - File discovery (source files, package.json, lock files)
148
+ * - Prettier configuration detection (once per project)
149
+ * - Parsing all files with appropriate parsers
150
+ * - Sharing PrettierConfigLoader across all JavaScript parsers
151
+ *
152
+ * Can be used directly for CLI tools or wrapped by RPC handlers.
153
+ */
154
+ export class ProjectParser {
155
+ private readonly projectPath: string;
156
+ private readonly exclusions: string[];
157
+ private readonly ctx: ExecutionContext;
158
+ private readonly useGit: boolean;
159
+ private readonly onProgress?: ProjectParserOptions["onProgress"];
160
+ private readonly verbose: boolean;
161
+ private readonly fileFilter?: (absolutePath: string) => boolean;
162
+
163
+ constructor(projectPath: string, options: ProjectParserOptions = {}) {
164
+ this.projectPath = path.resolve(projectPath);
165
+ this.exclusions = options.exclusions ?? DEFAULT_EXCLUSIONS;
166
+ this.ctx = options.ctx ?? new ExecutionContext();
167
+ this.useGit = options.useGit ?? this.isGitRepository();
168
+ this.onProgress = options.onProgress;
169
+ this.verbose = options.verbose ?? false;
170
+ this.fileFilter = options.fileFilter;
171
+ }
172
+
173
+ /**
174
+ * Creates and initializes a PrettierConfigLoader for this project.
175
+ * Use this when you need to handle Prettier detection separately from parsing.
176
+ */
177
+ async createPrettierLoader(): Promise<PrettierConfigLoader> {
178
+ this.log("Detecting Prettier configuration...");
179
+ const prettierLoader = new PrettierConfigLoader(this.projectPath);
180
+ await prettierLoader.detectPrettier();
181
+ return prettierLoader;
182
+ }
183
+
184
+ /**
185
+ * Builds an Autodetect marker from the given source files.
186
+ * Samples all files to detect common formatting styles.
187
+ * Uses dynamic import to avoid circular dependencies.
188
+ */
189
+ async buildAutodetectMarker(sourceFiles: SourceFile[]): Promise<Marker> {
190
+ // Dynamic import to break circular dependency at module load time:
191
+ // parse-project.ts → project-parser.ts → autodetect.ts → visitor.ts → java → rpc → parse-project.ts
192
+ // By deferring the import until runtime, all modules are already loaded when this executes.
193
+ const {Autodetect} = await import("./autodetect.js");
194
+ const {JS} = await import("./tree.js");
195
+
196
+ const detector = Autodetect.detector();
197
+ for (const sf of sourceFiles) {
198
+ if (sf.kind === JS.Kind.CompilationUnit) {
199
+ await detector.sample(sf);
200
+ }
201
+ }
202
+ return detector.build();
203
+ }
204
+
205
+ /**
206
+ * Parses all source files in the project.
207
+ * Yields source files as they are parsed.
208
+ */
209
+ async *parse(): AsyncGenerator<SourceFile> {
210
+ // Discover files
211
+ this.log("Discovering files...");
212
+ this.onProgress?.("discovering", 0, 0);
213
+ let discovered = await this.discoverFiles();
214
+
215
+ // Apply file filter if provided
216
+ if (this.fileFilter) {
217
+ discovered = this.applyFileFilter(discovered);
218
+ }
219
+
220
+ const totalFiles = this.countFiles(discovered);
221
+ this.log(`Found ${totalFiles} files to parse`);
222
+
223
+ // Detect Prettier configuration once for the project
224
+ const prettierLoader = await this.createPrettierLoader();
225
+
226
+ let current = 0;
227
+
228
+ // Parse package.json files first (they get NodeResolutionResult markers)
229
+ if (discovered.packageJsonFiles.length > 0) {
230
+ this.log(`Parsing ${discovered.packageJsonFiles.length} package.json files...`);
231
+ const parser = Parsers.createParser("packageJson", {
232
+ ctx: this.ctx,
233
+ relativeTo: this.projectPath
234
+ });
235
+ for await (const sf of parser.parse(...discovered.packageJsonFiles)) {
236
+ current++;
237
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
238
+ yield sf;
239
+ }
240
+ }
241
+
242
+ // Parse JSON lock files
243
+ if (discovered.lockFiles.json.length > 0) {
244
+ this.log(`Parsing ${discovered.lockFiles.json.length} JSON lock files...`);
245
+ const parser = Parsers.createParser("json", {
246
+ ctx: this.ctx,
247
+ relativeTo: this.projectPath
248
+ });
249
+ for await (const sf of parser.parse(...discovered.lockFiles.json)) {
250
+ current++;
251
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
252
+ yield sf;
253
+ }
254
+ }
255
+
256
+ // Parse YAML lock files
257
+ if (discovered.lockFiles.yaml.length > 0) {
258
+ this.log(`Parsing ${discovered.lockFiles.yaml.length} YAML lock files...`);
259
+ const parser = Parsers.createParser("yaml", {
260
+ ctx: this.ctx,
261
+ relativeTo: this.projectPath
262
+ });
263
+ for await (const sf of parser.parse(...discovered.lockFiles.yaml)) {
264
+ current++;
265
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
266
+ yield sf;
267
+ }
268
+ }
269
+
270
+ // Parse text lock files (yarn.lock Classic)
271
+ if (discovered.lockFiles.text.length > 0) {
272
+ this.log(`Parsing ${discovered.lockFiles.text.length} text lock files...`);
273
+ const parser = Parsers.createParser("plainText", {
274
+ ctx: this.ctx,
275
+ relativeTo: this.projectPath
276
+ });
277
+ for await (const sf of parser.parse(...discovered.lockFiles.text)) {
278
+ current++;
279
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
280
+ yield sf;
281
+ }
282
+ }
283
+
284
+ // Parse JavaScript/TypeScript source files
285
+ if (discovered.jsFiles.length > 0) {
286
+ this.log(`Parsing ${discovered.jsFiles.length} JavaScript/TypeScript files...`);
287
+ const parser = Parsers.createParser("javascript", {
288
+ ctx: this.ctx,
289
+ relativeTo: this.projectPath
290
+ });
291
+
292
+ // Check if Prettier is available
293
+ const detection = await prettierLoader.detectPrettier();
294
+
295
+ if (detection.available) {
296
+ // Prettier is available: add per-file PrettierStyle markers
297
+ for await (const sf of parser.parse(...discovered.jsFiles)) {
298
+ current++;
299
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
300
+
301
+ const prettierMarker = await prettierLoader.getConfigMarker(
302
+ path.join(this.projectPath, sf.sourcePath)
303
+ );
304
+ if (prettierMarker) {
305
+ yield produce(sf, draft => {
306
+ draft.markers.markers = draft.markers.markers.concat([prettierMarker]);
307
+ });
308
+ } else {
309
+ yield sf;
310
+ }
311
+ }
312
+ } else {
313
+ // Prettier is NOT available: auto-detect styles from parsed files
314
+ this.log("Prettier not found, auto-detecting styles...");
315
+
316
+ // Dynamic import to break circular dependency at module load time
317
+ // (see buildAutodetectMarker for explanation)
318
+ const {Autodetect} = await import("./autodetect.js");
319
+ const {JS} = await import("./tree.js");
320
+
321
+ const parsedFiles: SourceFile[] = [];
322
+
323
+ // Parse all JS files and collect them for sampling
324
+ for await (const sf of parser.parse(...discovered.jsFiles)) {
325
+ current++;
326
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
327
+ if (sf.kind === JS.Kind.CompilationUnit) {
328
+ parsedFiles.push(sf);
329
+ }
330
+ }
331
+
332
+ // Sample all parsed files and build Autodetect marker
333
+ const detector = Autodetect.detector();
334
+ for (const sf of parsedFiles) {
335
+ await detector.sample(sf);
336
+ }
337
+ const autodetectMarker = detector.build();
338
+ this.log(`Auto-detected styles: indent=${detector.getTabsAndIndentsStyle().indentSize}, ` +
339
+ `useTabs=${detector.getTabsAndIndentsStyle().useTabCharacter}`);
340
+
341
+ // Yield all files with the Autodetect marker
342
+ for (const sf of parsedFiles) {
343
+ yield produce(sf, draft => {
344
+ draft.markers.markers = draft.markers.markers.concat([autodetectMarker]);
345
+ });
346
+ }
347
+ }
348
+ }
349
+
350
+ // Parse other YAML files
351
+ if (discovered.yamlFiles.length > 0) {
352
+ this.log(`Parsing ${discovered.yamlFiles.length} YAML files...`);
353
+ const parser = Parsers.createParser("yaml", {
354
+ ctx: this.ctx,
355
+ relativeTo: this.projectPath
356
+ });
357
+ for await (const sf of parser.parse(...discovered.yamlFiles)) {
358
+ current++;
359
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
360
+ yield sf;
361
+ }
362
+ }
363
+
364
+ // Parse other JSON files
365
+ if (discovered.jsonFiles.length > 0) {
366
+ this.log(`Parsing ${discovered.jsonFiles.length} JSON files...`);
367
+ const parser = Parsers.createParser("json", {
368
+ ctx: this.ctx,
369
+ relativeTo: this.projectPath
370
+ });
371
+ for await (const sf of parser.parse(...discovered.jsonFiles)) {
372
+ current++;
373
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
374
+ yield sf;
375
+ }
376
+ }
377
+
378
+ // Parse text config files (.prettierignore, .gitignore, etc.)
379
+ if (discovered.textFiles.length > 0) {
380
+ this.log(`Parsing ${discovered.textFiles.length} text config files...`);
381
+ const parser = Parsers.createParser("plainText", {
382
+ ctx: this.ctx,
383
+ relativeTo: this.projectPath
384
+ });
385
+ for await (const sf of parser.parse(...discovered.textFiles)) {
386
+ current++;
387
+ this.onProgress?.("parsing", current, totalFiles, sf.sourcePath);
388
+ yield sf;
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Discovers all files in the project that should be parsed.
395
+ */
396
+ async discoverFiles(): Promise<DiscoveredFiles> {
397
+ const discovered: DiscoveredFiles = {
398
+ packageJsonFiles: [],
399
+ lockFiles: {
400
+ json: [],
401
+ yaml: [],
402
+ text: []
403
+ },
404
+ jsFiles: [],
405
+ jsonFiles: [],
406
+ yamlFiles: [],
407
+ textFiles: []
408
+ };
409
+
410
+ let files: string[];
411
+ if (this.useGit) {
412
+ files = await this.discoverFilesWithGit();
413
+ } else {
414
+ files = await this.discoverFilesWithWalk();
415
+ }
416
+
417
+ // Classify files
418
+ const yarnLockFiles: string[] = [];
419
+
420
+ for (const file of files) {
421
+ const basename = path.basename(file);
422
+ const ext = path.extname(file).toLowerCase();
423
+
424
+ if (basename === "package.json") {
425
+ discovered.packageJsonFiles.push(file);
426
+ } else if (SOURCE_EXTENSIONS.has(ext)) {
427
+ discovered.jsFiles.push(file);
428
+ } else if ((JSON_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
429
+ discovered.lockFiles.json.push(file);
430
+ } else if (basename === "yarn.lock") {
431
+ // yarn.lock needs content-based classification
432
+ yarnLockFiles.push(file);
433
+ } else if ((YAML_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
434
+ discovered.lockFiles.yaml.push(file);
435
+ } else if ((TEXT_LOCK_FILE_NAMES as readonly string[]).includes(basename)) {
436
+ discovered.lockFiles.text.push(file);
437
+ } else if (ext === ".json") {
438
+ discovered.jsonFiles.push(file);
439
+ } else if (ext === ".yaml" || ext === ".yml") {
440
+ discovered.yamlFiles.push(file);
441
+ } else if (TEXT_CONFIG_FILES.has(basename)) {
442
+ discovered.textFiles.push(file);
443
+ }
444
+ }
445
+
446
+ // Classify yarn.lock files by content
447
+ for (const yarnLockPath of yarnLockFiles) {
448
+ const format = await this.classifyYarnLockFile(yarnLockPath);
449
+ if (format === "yaml") {
450
+ discovered.lockFiles.yaml.push(yarnLockPath);
451
+ } else {
452
+ discovered.lockFiles.text.push(yarnLockPath);
453
+ }
454
+ }
455
+
456
+ return discovered;
457
+ }
458
+
459
+ /**
460
+ * Discovers files using git ls-files (respects .gitignore).
461
+ */
462
+ private async discoverFilesWithGit(): Promise<string[]> {
463
+ const files: string[] = [];
464
+
465
+ // Get tracked files
466
+ const tracked = spawnSync("git", ["ls-files"], {
467
+ cwd: this.projectPath,
468
+ encoding: "utf8"
469
+ });
470
+
471
+ if (tracked.status !== 0 || tracked.error) {
472
+ // Fall back to walk if git fails
473
+ return this.discoverFilesWithWalk();
474
+ }
475
+
476
+ if (tracked.stdout) {
477
+ for (const line of tracked.stdout.split("\n")) {
478
+ const trimmed = line.trim();
479
+ if (trimmed) {
480
+ const fullPath = path.join(this.projectPath, trimmed);
481
+ // git ls-files can return deleted files that are still tracked
482
+ if (fs.existsSync(fullPath)) {
483
+ files.push(fullPath);
484
+ }
485
+ }
486
+ }
487
+ }
488
+
489
+ // Get untracked but not ignored files
490
+ const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], {
491
+ cwd: this.projectPath,
492
+ encoding: "utf8"
493
+ });
494
+
495
+ if (untracked.stdout) {
496
+ for (const line of untracked.stdout.split("\n")) {
497
+ const trimmed = line.trim();
498
+ if (trimmed) {
499
+ // Untracked files should exist, but check anyway for robustness
500
+ const fullPath = path.join(this.projectPath, trimmed);
501
+ if (fs.existsSync(fullPath)) {
502
+ files.push(fullPath);
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ // Filter by our exclusion patterns and accepted file types
509
+ return files.filter(file => this.isAcceptedFile(file));
510
+ }
511
+
512
+ /**
513
+ * Discovers files by walking the directory tree.
514
+ */
515
+ private async discoverFilesWithWalk(): Promise<string[]> {
516
+ const files: string[] = [];
517
+ const isExcluded = picomatch(this.exclusions);
518
+
519
+ const walk = async (dir: string, relativePath: string = "") => {
520
+ let entries: fs.Dirent[];
521
+ try {
522
+ entries = await fsp.readdir(dir, {withFileTypes: true});
523
+ } catch {
524
+ return;
525
+ }
526
+
527
+ for (const entry of entries) {
528
+ const fullPath = path.join(dir, entry.name);
529
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
530
+
531
+ // Check exclusion patterns
532
+ if (isExcluded(relPath) || isExcluded(`${relPath}/`)) {
533
+ continue;
534
+ }
535
+
536
+ // Always skip node_modules
537
+ if (entry.name === "node_modules") {
538
+ continue;
539
+ }
540
+
541
+ if (entry.isDirectory()) {
542
+ await walk(fullPath, relPath);
543
+ } else if (entry.isFile() && this.isAcceptedFile(fullPath)) {
544
+ files.push(fullPath);
545
+ }
546
+ }
547
+ };
548
+
549
+ await walk(this.projectPath);
550
+ return files;
551
+ }
552
+
553
+ /**
554
+ * Checks if a file should be accepted for parsing.
555
+ */
556
+ private isAcceptedFile(filePath: string): boolean {
557
+ const basename = path.basename(filePath);
558
+ const ext = path.extname(filePath).toLowerCase();
559
+
560
+ // Check exclusion patterns with relative path
561
+ const relativePath = path.relative(this.projectPath, filePath);
562
+ const isExcluded = picomatch(this.exclusions);
563
+ if (isExcluded(relativePath)) {
564
+ return false;
565
+ }
566
+
567
+ // JavaScript/TypeScript files
568
+ if (SOURCE_EXTENSIONS.has(ext)) {
569
+ return true;
570
+ }
571
+
572
+ // JSON files
573
+ if (ext === ".json") {
574
+ return true;
575
+ }
576
+
577
+ // YAML files
578
+ if (ext === ".yaml" || ext === ".yml") {
579
+ return true;
580
+ }
581
+
582
+ // Lock files (some have non-standard extensions)
583
+ if (ALL_LOCK_FILE_NAMES.has(basename)) {
584
+ return true;
585
+ }
586
+
587
+ // Text config files
588
+ if (TEXT_CONFIG_FILES.has(basename)) {
589
+ return true;
590
+ }
591
+
592
+ return false;
593
+ }
594
+
595
+ /**
596
+ * Classifies a yarn.lock file as YAML (Berry) or text (Classic).
597
+ */
598
+ private async classifyYarnLockFile(filePath: string): Promise<"yaml" | "text"> {
599
+ try {
600
+ const content = await fsp.readFile(filePath, "utf-8");
601
+ return isYarnBerryLockFile(content) ? "yaml" : "text";
602
+ } catch {
603
+ return "text";
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Checks if the project is a git repository.
609
+ */
610
+ private isGitRepository(): boolean {
611
+ return fs.existsSync(path.join(this.projectPath, ".git"));
612
+ }
613
+
614
+ /**
615
+ * Counts total files to parse.
616
+ */
617
+ private countFiles(discovered: DiscoveredFiles): number {
618
+ return (
619
+ discovered.packageJsonFiles.length +
620
+ discovered.lockFiles.json.length +
621
+ discovered.lockFiles.yaml.length +
622
+ discovered.lockFiles.text.length +
623
+ discovered.jsFiles.length +
624
+ discovered.jsonFiles.length +
625
+ discovered.yamlFiles.length +
626
+ discovered.textFiles.length
627
+ );
628
+ }
629
+
630
+ /**
631
+ * Applies the file filter to discovered files.
632
+ */
633
+ private applyFileFilter(discovered: DiscoveredFiles): DiscoveredFiles {
634
+ const filter = this.fileFilter!;
635
+ return {
636
+ packageJsonFiles: discovered.packageJsonFiles.filter(filter),
637
+ lockFiles: {
638
+ json: discovered.lockFiles.json.filter(filter),
639
+ yaml: discovered.lockFiles.yaml.filter(filter),
640
+ text: discovered.lockFiles.text.filter(filter)
641
+ },
642
+ jsFiles: discovered.jsFiles.filter(filter),
643
+ jsonFiles: discovered.jsonFiles.filter(filter),
644
+ yamlFiles: discovered.yamlFiles.filter(filter),
645
+ textFiles: discovered.textFiles.filter(filter)
646
+ };
647
+ }
648
+
649
+ /**
650
+ * Logs a message if verbose mode is enabled.
651
+ */
652
+ private log(message: string): void {
653
+ if (this.verbose) {
654
+ console.log(message);
655
+ }
656
+ }
657
+ }