@webpieces/dev-config 0.2.72 → 0.2.74

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.
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Validate No Inline Types Executor
3
+ *
4
+ * Validates that inline type literals are not used - prefer named types/interfaces/classes.
5
+ * Instead of anonymous object types, use named types for clarity and reusability:
6
+ *
7
+ * BAD: function foo(arg: { x: number }) { }
8
+ * GOOD: function foo(arg: MyConfig) { }
9
+ *
10
+ * BAD: type Nullable = { x: number } | null;
11
+ * GOOD: type MyData = { x: number }; type Nullable = MyData | null;
12
+ *
13
+ * Modes:
14
+ * - OFF: Skip validation entirely
15
+ * - NEW_METHODS: Only validate inline types in new methods (detected via git diff)
16
+ * - MODIFIED_AND_NEW_METHODS: Validate in new methods + methods with changes in their line range
17
+ * - MODIFIED_FILES: Validate all inline types in modified files
18
+ * - ALL: Validate all inline types in all TypeScript files
19
+ *
20
+ * Escape hatch: Add webpieces-disable no-inline-types comment with justification
21
+ */
22
+
23
+ import type { ExecutorContext } from '@nx/devkit';
24
+ import { execSync } from 'child_process';
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ import * as ts from 'typescript';
28
+
29
+ export type NoInlineTypesMode = 'OFF' | 'NEW_METHODS' | 'MODIFIED_AND_NEW_METHODS' | 'MODIFIED_FILES' | 'ALL';
30
+
31
+ export interface ValidateNoInlineTypesOptions {
32
+ mode?: NoInlineTypesMode;
33
+ }
34
+
35
+ export interface ExecutorResult {
36
+ success: boolean;
37
+ }
38
+
39
+ interface InlineTypeViolation {
40
+ file: string;
41
+ line: number;
42
+ column: number;
43
+ context: string;
44
+ }
45
+
46
+ /**
47
+ * Get changed TypeScript files between base and head (or working tree if head not specified).
48
+ */
49
+ // webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
50
+ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: string): string[] {
51
+ try {
52
+ const diffTarget = head ? `${base} ${head}` : base;
53
+ const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
54
+ cwd: workspaceRoot,
55
+ encoding: 'utf-8',
56
+ });
57
+ const changedFiles = output
58
+ .trim()
59
+ .split('\n')
60
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
61
+
62
+ if (!head) {
63
+ try {
64
+ const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
65
+ cwd: workspaceRoot,
66
+ encoding: 'utf-8',
67
+ });
68
+ const untrackedFiles = untrackedOutput
69
+ .trim()
70
+ .split('\n')
71
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
72
+ const allFiles = new Set([...changedFiles, ...untrackedFiles]);
73
+ return Array.from(allFiles);
74
+ } catch {
75
+ return changedFiles;
76
+ }
77
+ }
78
+
79
+ return changedFiles;
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all TypeScript files in the workspace using git ls-files (excluding tests).
87
+ */
88
+ function getAllTypeScriptFiles(workspaceRoot: string): string[] {
89
+ try {
90
+ const output = execSync(`git ls-files '*.ts' '*.tsx'`, {
91
+ cwd: workspaceRoot,
92
+ encoding: 'utf-8',
93
+ });
94
+ return output
95
+ .trim()
96
+ .split('\n')
97
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get the diff content for a specific file.
105
+ */
106
+ function getFileDiff(workspaceRoot: string, file: string, base: string, head?: string): string {
107
+ try {
108
+ const diffTarget = head ? `${base} ${head}` : base;
109
+ const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
110
+ cwd: workspaceRoot,
111
+ encoding: 'utf-8',
112
+ });
113
+
114
+ if (!diff && !head) {
115
+ const fullPath = path.join(workspaceRoot, file);
116
+ if (fs.existsSync(fullPath)) {
117
+ const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
118
+ cwd: workspaceRoot,
119
+ encoding: 'utf-8',
120
+ }).trim();
121
+
122
+ if (isUntracked) {
123
+ const content = fs.readFileSync(fullPath, 'utf-8');
124
+ const lines = content.split('\n');
125
+ return lines.map((line) => `+${line}`).join('\n');
126
+ }
127
+ }
128
+ }
129
+
130
+ return diff;
131
+ } catch {
132
+ return '';
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Parse diff to extract changed line numbers (both additions and modifications).
138
+ */
139
+ function getChangedLineNumbers(diffContent: string): Set<number> {
140
+ const changedLines = new Set<number>();
141
+ const lines = diffContent.split('\n');
142
+ let currentLine = 0;
143
+
144
+ for (const line of lines) {
145
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
146
+ if (hunkMatch) {
147
+ currentLine = parseInt(hunkMatch[1], 10);
148
+ continue;
149
+ }
150
+
151
+ if (line.startsWith('+') && !line.startsWith('+++')) {
152
+ changedLines.add(currentLine);
153
+ currentLine++;
154
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
155
+ // Deletions don't increment line number
156
+ } else {
157
+ currentLine++;
158
+ }
159
+ }
160
+
161
+ return changedLines;
162
+ }
163
+
164
+ /**
165
+ * Check if a line contains a webpieces-disable comment for no-inline-types.
166
+ */
167
+ function hasDisableComment(lines: string[], lineNumber: number): boolean {
168
+ const startCheck = Math.max(0, lineNumber - 5);
169
+ for (let i = lineNumber - 2; i >= startCheck; i--) {
170
+ const line = lines[i]?.trim() ?? '';
171
+ if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
172
+ break;
173
+ }
174
+ if (line.includes('webpieces-disable') && line.includes('no-inline-types')) {
175
+ return true;
176
+ }
177
+ }
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Check if a TypeLiteral node is in an allowed context.
183
+ * Only allowed if the DIRECT parent is a TypeAliasDeclaration.
184
+ */
185
+ function isInAllowedContext(node: ts.TypeLiteralNode): boolean {
186
+ const parent = node.parent;
187
+ // Only allowed if it's the DIRECT body of a type alias
188
+ if (ts.isTypeAliasDeclaration(parent)) {
189
+ return true;
190
+ }
191
+ return false;
192
+ }
193
+
194
+ /**
195
+ * Get a description of the context where the inline type appears.
196
+ */
197
+ // webpieces-disable max-lines-new-methods -- Context detection requires checking many AST node types
198
+ function getViolationContext(node: ts.TypeLiteralNode, sourceFile: ts.SourceFile): string {
199
+ let current: ts.Node = node;
200
+ while (current.parent) {
201
+ const parent = current.parent;
202
+ if (ts.isParameter(parent)) {
203
+ return 'inline parameter type';
204
+ }
205
+ if (ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isArrowFunction(parent)) {
206
+ if (parent.type === current) {
207
+ return 'inline return type';
208
+ }
209
+ }
210
+ if (ts.isVariableDeclaration(parent)) {
211
+ return 'inline variable type';
212
+ }
213
+ if (ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent)) {
214
+ if (parent.type === current) {
215
+ return 'inline property type';
216
+ }
217
+ // Check if it's nested inside another type literal
218
+ let ancestor: ts.Node | undefined = parent.parent;
219
+ while (ancestor) {
220
+ if (ts.isTypeLiteralNode(ancestor)) {
221
+ return 'nested inline type';
222
+ }
223
+ if (ts.isTypeAliasDeclaration(ancestor)) {
224
+ return 'nested inline type in type alias';
225
+ }
226
+ ancestor = ancestor.parent;
227
+ }
228
+ }
229
+ if (ts.isUnionTypeNode(parent) || ts.isIntersectionTypeNode(parent)) {
230
+ return 'inline type in union/intersection';
231
+ }
232
+ if (ts.isTypeReferenceNode(parent.parent) && ts.isTypeNode(parent)) {
233
+ return 'inline type in generic argument';
234
+ }
235
+ current = parent;
236
+ }
237
+ return 'inline type literal';
238
+ }
239
+
240
+ interface MethodInfo {
241
+ name: string;
242
+ startLine: number;
243
+ endLine: number;
244
+ }
245
+
246
+ /**
247
+ * Find all methods/functions in a file with their line ranges.
248
+ */
249
+ // webpieces-disable max-lines-new-methods -- AST traversal requires inline visitor function
250
+ function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[] {
251
+ const fullPath = path.join(workspaceRoot, filePath);
252
+ if (!fs.existsSync(fullPath)) return [];
253
+
254
+ const content = fs.readFileSync(fullPath, 'utf-8');
255
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
256
+
257
+ const methods: MethodInfo[] = [];
258
+
259
+ // webpieces-disable max-lines-new-methods -- AST visitor pattern requires handling multiple node types
260
+ function visit(node: ts.Node): void {
261
+ let methodName: string | undefined;
262
+ let startLine: number | undefined;
263
+ let endLine: number | undefined;
264
+
265
+ if (ts.isMethodDeclaration(node) && node.name) {
266
+ methodName = node.name.getText(sourceFile);
267
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
268
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
269
+ startLine = start.line + 1;
270
+ endLine = end.line + 1;
271
+ } else if (ts.isFunctionDeclaration(node) && node.name) {
272
+ methodName = node.name.getText(sourceFile);
273
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
274
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
275
+ startLine = start.line + 1;
276
+ endLine = end.line + 1;
277
+ } else if (ts.isArrowFunction(node)) {
278
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
279
+ methodName = node.parent.name.getText(sourceFile);
280
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
281
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
282
+ startLine = start.line + 1;
283
+ endLine = end.line + 1;
284
+ }
285
+ }
286
+
287
+ if (methodName && startLine !== undefined && endLine !== undefined) {
288
+ methods.push({ name: methodName, startLine, endLine });
289
+ }
290
+
291
+ ts.forEachChild(node, visit);
292
+ }
293
+
294
+ visit(sourceFile);
295
+ return methods;
296
+ }
297
+
298
+ /**
299
+ * Check if a line is within any method's range and if that method has changes.
300
+ */
301
+ function isLineInChangedMethod(
302
+ line: number,
303
+ methods: MethodInfo[],
304
+ changedLines: Set<number>,
305
+ newMethodNames: Set<string>
306
+ ): boolean {
307
+ for (const method of methods) {
308
+ if (line >= method.startLine && line <= method.endLine) {
309
+ // Check if this method is new or has changes
310
+ if (newMethodNames.has(method.name)) {
311
+ return true;
312
+ }
313
+ // Check if any line in the method range has changes
314
+ for (let l = method.startLine; l <= method.endLine; l++) {
315
+ if (changedLines.has(l)) {
316
+ return true;
317
+ }
318
+ }
319
+ }
320
+ }
321
+ return false;
322
+ }
323
+
324
+ /**
325
+ * Check if a line is within a new method.
326
+ */
327
+ function isLineInNewMethod(line: number, methods: MethodInfo[], newMethodNames: Set<string>): boolean {
328
+ for (const method of methods) {
329
+ if (line >= method.startLine && line <= method.endLine && newMethodNames.has(method.name)) {
330
+ return true;
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * Parse diff to find newly added method signatures.
338
+ */
339
+ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
340
+ const newMethods = new Set<string>();
341
+ const lines = diffContent.split('\n');
342
+
343
+ const patterns = [
344
+ /^\+\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
345
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
346
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
347
+ /^\+\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(/,
348
+ ];
349
+
350
+ for (const line of lines) {
351
+ if (line.startsWith('+') && !line.startsWith('+++')) {
352
+ for (const pattern of patterns) {
353
+ const match = line.match(pattern);
354
+ if (match) {
355
+ const methodName = match[1];
356
+ if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
357
+ newMethods.add(methodName);
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ return newMethods;
366
+ }
367
+
368
+ interface InlineTypeInfo {
369
+ line: number;
370
+ column: number;
371
+ context: string;
372
+ hasDisableComment: boolean;
373
+ }
374
+
375
+ /**
376
+ * Find all inline type literals in a file.
377
+ */
378
+ // webpieces-disable max-lines-new-methods -- AST traversal with visitor pattern
379
+ function findInlineTypesInFile(filePath: string, workspaceRoot: string): InlineTypeInfo[] {
380
+ const fullPath = path.join(workspaceRoot, filePath);
381
+ if (!fs.existsSync(fullPath)) return [];
382
+
383
+ const content = fs.readFileSync(fullPath, 'utf-8');
384
+ const fileLines = content.split('\n');
385
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
386
+
387
+ const inlineTypes: InlineTypeInfo[] = [];
388
+
389
+ function visit(node: ts.Node): void {
390
+ if (ts.isTypeLiteralNode(node)) {
391
+ if (!isInAllowedContext(node)) {
392
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
393
+ const line = pos.line + 1;
394
+ const column = pos.character + 1;
395
+ const context = getViolationContext(node, sourceFile);
396
+ const disabled = hasDisableComment(fileLines, line);
397
+
398
+ inlineTypes.push({
399
+ line,
400
+ column,
401
+ context,
402
+ hasDisableComment: disabled,
403
+ });
404
+ }
405
+ }
406
+ ts.forEachChild(node, visit);
407
+ }
408
+
409
+ visit(sourceFile);
410
+ return inlineTypes;
411
+ }
412
+
413
+ /**
414
+ * Find violations in new methods only (NEW_METHODS mode).
415
+ */
416
+ // webpieces-disable max-lines-new-methods -- File iteration with diff parsing and method matching
417
+ function findViolationsForNewMethods(
418
+ workspaceRoot: string,
419
+ changedFiles: string[],
420
+ base: string,
421
+ head?: string
422
+ ): InlineTypeViolation[] {
423
+ const violations: InlineTypeViolation[] = [];
424
+
425
+ for (const file of changedFiles) {
426
+ const diff = getFileDiff(workspaceRoot, file, base, head);
427
+ const newMethodNames = findNewMethodSignaturesInDiff(diff);
428
+
429
+ if (newMethodNames.size === 0) continue;
430
+
431
+ const methods = findMethodsInFile(file, workspaceRoot);
432
+ const inlineTypes = findInlineTypesInFile(file, workspaceRoot);
433
+
434
+ for (const inlineType of inlineTypes) {
435
+ if (inlineType.hasDisableComment) continue;
436
+ if (!isLineInNewMethod(inlineType.line, methods, newMethodNames)) continue;
437
+
438
+ violations.push({
439
+ file,
440
+ line: inlineType.line,
441
+ column: inlineType.column,
442
+ context: inlineType.context,
443
+ });
444
+ }
445
+ }
446
+
447
+ return violations;
448
+ }
449
+
450
+ /**
451
+ * Find violations in new and modified methods (MODIFIED_AND_NEW_METHODS mode).
452
+ */
453
+ // webpieces-disable max-lines-new-methods -- Combines new method detection with change detection
454
+ function findViolationsForModifiedAndNewMethods(
455
+ workspaceRoot: string,
456
+ changedFiles: string[],
457
+ base: string,
458
+ head?: string
459
+ ): InlineTypeViolation[] {
460
+ const violations: InlineTypeViolation[] = [];
461
+
462
+ for (const file of changedFiles) {
463
+ const diff = getFileDiff(workspaceRoot, file, base, head);
464
+ const newMethodNames = findNewMethodSignaturesInDiff(diff);
465
+ const changedLines = getChangedLineNumbers(diff);
466
+
467
+ const methods = findMethodsInFile(file, workspaceRoot);
468
+ const inlineTypes = findInlineTypesInFile(file, workspaceRoot);
469
+
470
+ for (const inlineType of inlineTypes) {
471
+ if (inlineType.hasDisableComment) continue;
472
+ if (!isLineInChangedMethod(inlineType.line, methods, changedLines, newMethodNames)) continue;
473
+
474
+ violations.push({
475
+ file,
476
+ line: inlineType.line,
477
+ column: inlineType.column,
478
+ context: inlineType.context,
479
+ });
480
+ }
481
+ }
482
+
483
+ return violations;
484
+ }
485
+
486
+ /**
487
+ * Find all violations in modified files (MODIFIED_FILES mode).
488
+ */
489
+ function findViolationsForModifiedFiles(workspaceRoot: string, changedFiles: string[]): InlineTypeViolation[] {
490
+ const violations: InlineTypeViolation[] = [];
491
+
492
+ for (const file of changedFiles) {
493
+ const inlineTypes = findInlineTypesInFile(file, workspaceRoot);
494
+
495
+ for (const inlineType of inlineTypes) {
496
+ if (inlineType.hasDisableComment) continue;
497
+
498
+ violations.push({
499
+ file,
500
+ line: inlineType.line,
501
+ column: inlineType.column,
502
+ context: inlineType.context,
503
+ });
504
+ }
505
+ }
506
+
507
+ return violations;
508
+ }
509
+
510
+ /**
511
+ * Find all violations in all files (ALL mode).
512
+ */
513
+ function findViolationsForAll(workspaceRoot: string): InlineTypeViolation[] {
514
+ const allFiles = getAllTypeScriptFiles(workspaceRoot);
515
+ return findViolationsForModifiedFiles(workspaceRoot, allFiles);
516
+ }
517
+
518
+ /**
519
+ * Auto-detect the base branch by finding the merge-base with origin/main.
520
+ */
521
+ function detectBase(workspaceRoot: string): string | null {
522
+ try {
523
+ const mergeBase = execSync('git merge-base HEAD origin/main', {
524
+ cwd: workspaceRoot,
525
+ encoding: 'utf-8',
526
+ stdio: ['pipe', 'pipe', 'pipe'],
527
+ }).trim();
528
+
529
+ if (mergeBase) {
530
+ return mergeBase;
531
+ }
532
+ } catch {
533
+ try {
534
+ const mergeBase = execSync('git merge-base HEAD main', {
535
+ cwd: workspaceRoot,
536
+ encoding: 'utf-8',
537
+ stdio: ['pipe', 'pipe', 'pipe'],
538
+ }).trim();
539
+
540
+ if (mergeBase) {
541
+ return mergeBase;
542
+ }
543
+ } catch {
544
+ // Ignore
545
+ }
546
+ }
547
+ return null;
548
+ }
549
+
550
+ /**
551
+ * Report violations to console.
552
+ */
553
+ function reportViolations(violations: InlineTypeViolation[], mode: NoInlineTypesMode): void {
554
+ console.error('');
555
+ console.error('āŒ Inline type literals found! Use named types instead.');
556
+ console.error('');
557
+ console.error('šŸ“š Named types improve code clarity and reusability:');
558
+ console.error('');
559
+ console.error(' BAD: function foo(arg: { x: number }) { }');
560
+ console.error(' GOOD: type MyConfig = { x: number };');
561
+ console.error(' function foo(arg: MyConfig) { }');
562
+ console.error('');
563
+ console.error(' BAD: type Nullable = { x: number } | null;');
564
+ console.error(' GOOD: type MyData = { x: number };');
565
+ console.error(' type Nullable = MyData | null;');
566
+ console.error('');
567
+
568
+ for (const v of violations) {
569
+ console.error(` āŒ ${v.file}:${v.line}:${v.column}`);
570
+ console.error(` ${v.context}`);
571
+ }
572
+ console.error('');
573
+
574
+ console.error(' To fix: Extract inline types to named type aliases or interfaces');
575
+ console.error('');
576
+ console.error(' Escape hatch (use sparingly):');
577
+ console.error(' // webpieces-disable no-inline-types -- [your reason]');
578
+ console.error('');
579
+ console.error(` Current mode: ${mode}`);
580
+ console.error('');
581
+ }
582
+
583
+ export default async function runExecutor(
584
+ options: ValidateNoInlineTypesOptions,
585
+ context: ExecutorContext
586
+ ): Promise<ExecutorResult> {
587
+ const workspaceRoot = context.root;
588
+ const mode: NoInlineTypesMode = options.mode ?? 'OFF';
589
+
590
+ if (mode === 'OFF') {
591
+ console.log('\nā­ļø Skipping no-inline-types validation (mode: OFF)');
592
+ console.log('');
593
+ return { success: true };
594
+ }
595
+
596
+ console.log('\nšŸ“ Validating No Inline Types\n');
597
+ console.log(` Mode: ${mode}`);
598
+
599
+ let violations: InlineTypeViolation[] = [];
600
+
601
+ if (mode === 'ALL') {
602
+ console.log(' Scope: All tracked TypeScript files');
603
+ console.log('');
604
+ violations = findViolationsForAll(workspaceRoot);
605
+ } else {
606
+ let base = process.env['NX_BASE'];
607
+ const head = process.env['NX_HEAD'];
608
+
609
+ if (!base) {
610
+ base = detectBase(workspaceRoot) ?? undefined;
611
+
612
+ if (!base) {
613
+ console.log('\nā­ļø Skipping no-inline-types validation (could not detect base branch)');
614
+ console.log('');
615
+ return { success: true };
616
+ }
617
+ }
618
+
619
+ console.log(` Base: ${base}`);
620
+ console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
621
+ console.log('');
622
+
623
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
624
+
625
+ if (changedFiles.length === 0) {
626
+ console.log('āœ… No TypeScript files changed');
627
+ return { success: true };
628
+ }
629
+
630
+ console.log(`šŸ“‚ Checking ${changedFiles.length} changed file(s)...`);
631
+
632
+ if (mode === 'NEW_METHODS') {
633
+ violations = findViolationsForNewMethods(workspaceRoot, changedFiles, base, head);
634
+ } else if (mode === 'MODIFIED_AND_NEW_METHODS') {
635
+ violations = findViolationsForModifiedAndNewMethods(workspaceRoot, changedFiles, base, head);
636
+ } else if (mode === 'MODIFIED_FILES') {
637
+ violations = findViolationsForModifiedFiles(workspaceRoot, changedFiles);
638
+ }
639
+ }
640
+
641
+ if (violations.length === 0) {
642
+ console.log('āœ… No inline type literals found');
643
+ return { success: true };
644
+ }
645
+
646
+ reportViolations(violations, mode);
647
+
648
+ return { success: false };
649
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "title": "Validate No Inline Types Executor",
4
+ "description": "Validates that inline type literals are not used - prefer named types/interfaces/classes.",
5
+ "type": "object",
6
+ "properties": {
7
+ "mode": {
8
+ "type": "string",
9
+ "enum": ["OFF", "NEW_METHODS", "MODIFIED_AND_NEW_METHODS", "MODIFIED_FILES", "ALL"],
10
+ "description": "OFF: skip validation. NEW_METHODS: only new methods in diff. MODIFIED_AND_NEW_METHODS: new methods + methods with changes. MODIFIED_FILES: all inline types in modified files. ALL: all inline types everywhere.",
11
+ "default": "OFF"
12
+ }
13
+ },
14
+ "required": []
15
+ }
@@ -10,14 +10,15 @@
10
10
  *
11
11
  * Modes:
12
12
  * - OFF: Skip validation entirely
13
- * - MODIFIED_NEW: Only validate new methods (detected via git diff)
14
- * - MODIFIED: Validate all methods in modified files
13
+ * - NEW_METHODS: Only validate new methods (detected via git diff)
14
+ * - MODIFIED_AND_NEW_METHODS: Validate new methods + methods with changes in their line range
15
+ * - MODIFIED_FILES: Validate all methods in modified files
15
16
  * - ALL: Validate all methods in all TypeScript files
16
17
  *
17
18
  * Escape hatch: Add webpieces-disable require-return-type comment with justification
18
19
  */
19
20
  import type { ExecutorContext } from '@nx/devkit';
20
- export type ReturnTypeMode = 'OFF' | 'MODIFIED_NEW' | 'MODIFIED' | 'ALL';
21
+ export type ReturnTypeMode = 'OFF' | 'NEW_METHODS' | 'MODIFIED_AND_NEW_METHODS' | 'MODIFIED_FILES' | 'ALL';
21
22
  export interface ValidateReturnTypesOptions {
22
23
  mode?: ReturnTypeMode;
23
24
  }