@webpieces/code-rules 0.0.1 → 0.2.114
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.
- package/package.json +4 -3
- package/src/cli.d.ts +1 -0
- package/src/cli.js +19 -0
- package/src/cli.js.map +1 -0
- package/src/diff-utils.d.ts +24 -0
- package/src/{diff-utils.ts → diff-utils.js} +30 -38
- package/src/diff-utils.js.map +1 -0
- package/src/from-shared-config.d.ts +28 -0
- package/src/from-shared-config.js +119 -0
- package/src/from-shared-config.js.map +1 -0
- package/src/index.js +33 -0
- package/src/index.js.map +1 -0
- package/src/validate-catch-error-pattern.d.ts +47 -0
- package/src/{validate-catch-error-pattern.ts → validate-catch-error-pattern.js} +74 -195
- package/src/validate-catch-error-pattern.js.map +1 -0
- package/src/validate-code.d.ts +98 -0
- package/src/{validate-code.ts → validate-code.js} +65 -259
- package/src/validate-code.js.map +1 -0
- package/src/validate-dtos.d.ts +41 -0
- package/src/{validate-dtos.ts → validate-dtos.js} +88 -215
- package/src/validate-dtos.js.map +1 -0
- package/src/validate-modified-files.d.ts +24 -0
- package/src/{validate-modified-files.ts → validate-modified-files.js} +46 -115
- package/src/validate-modified-files.js.map +1 -0
- package/src/validate-modified-methods.d.ts +30 -0
- package/src/{validate-modified-methods.ts → validate-modified-methods.js} +94 -196
- package/src/validate-modified-methods.js.map +1 -0
- package/src/validate-new-methods.d.ts +27 -0
- package/src/{validate-new-methods.ts → validate-new-methods.js} +63 -133
- package/src/validate-new-methods.js.map +1 -0
- package/src/validate-no-any-unknown.d.ts +41 -0
- package/src/{validate-no-any-unknown.ts → validate-no-any-unknown.js} +69 -146
- package/src/validate-no-any-unknown.js.map +1 -0
- package/src/validate-no-destructure.d.ts +51 -0
- package/src/{validate-no-destructure.ts → validate-no-destructure.js} +80 -166
- package/src/validate-no-destructure.js.map +1 -0
- package/src/validate-no-direct-api-resolver.d.ts +46 -0
- package/src/{validate-no-direct-api-resolver.ts → validate-no-direct-api-resolver.js} +112 -211
- package/src/validate-no-direct-api-resolver.js.map +1 -0
- package/src/validate-no-implicit-any.d.ts +36 -0
- package/src/{validate-no-implicit-any.ts → validate-no-implicit-any.js} +94 -141
- package/src/validate-no-implicit-any.js.map +1 -0
- package/src/validate-no-inline-types.d.ts +90 -0
- package/src/{validate-no-inline-types.ts → validate-no-inline-types.js} +93 -198
- package/src/validate-no-inline-types.js.map +1 -0
- package/src/validate-no-unmanaged-exceptions.d.ts +43 -0
- package/src/{validate-no-unmanaged-exceptions.ts → validate-no-unmanaged-exceptions.js} +71 -140
- package/src/validate-no-unmanaged-exceptions.js.map +1 -0
- package/src/validate-prisma-converters.d.ts +59 -0
- package/src/{validate-prisma-converters.ts → validate-prisma-converters.js} +120 -307
- package/src/validate-prisma-converters.js.map +1 -0
- package/src/validate-return-types.d.ts +28 -0
- package/src/{validate-return-types.ts → validate-return-types.js} +84 -168
- package/src/validate-return-types.js.map +1 -0
- package/LICENSE +0 -373
- package/jest.config.ts +0 -20
- package/project.json +0 -22
- package/src/cli.ts +0 -17
- package/src/from-shared-config.ts +0 -118
- package/tsconfig.json +0 -22
- package/tsconfig.lib.json +0 -10
- package/tsconfig.spec.json +0 -14
- /package/src/{index.ts → index.d.ts} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Validate Modified Methods Executor
|
|
3
4
|
*
|
|
@@ -18,36 +19,15 @@
|
|
|
18
19
|
* Format: // webpieces-disable max-lines-modified 2025/01/15 -- [reason]
|
|
19
20
|
* The disable expires after 1 month from the date specified.
|
|
20
21
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export interface ValidateModifiedMethodsOptions {
|
|
30
|
-
limit?: number;
|
|
31
|
-
mode?: MethodMaxLimitMode;
|
|
32
|
-
disableAllowed?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ExecutorResult {
|
|
36
|
-
success: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface MethodViolation {
|
|
40
|
-
file: string;
|
|
41
|
-
methodName: string;
|
|
42
|
-
line: number;
|
|
43
|
-
lines: number;
|
|
44
|
-
expiredDisable?: boolean;
|
|
45
|
-
expiredDate?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.default = runValidator;
|
|
24
|
+
const tslib_1 = require("tslib");
|
|
25
|
+
const child_process_1 = require("child_process");
|
|
26
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
27
|
+
const path = tslib_1.__importStar(require("path"));
|
|
28
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
48
29
|
const TMP_DIR = '.webpieces/instruct-ai';
|
|
49
30
|
const TMP_MD_FILE = 'webpieces.methodsize.md';
|
|
50
|
-
|
|
51
31
|
const METHODSIZE_DOC_CONTENT = `# Instructions: Method Too Long
|
|
52
32
|
|
|
53
33
|
## Requirement
|
|
@@ -172,31 +152,27 @@ This ensures that disable comments are reviewed periodically.
|
|
|
172
152
|
- The best code explains itself through structure
|
|
173
153
|
- When in doubt, extract and name it
|
|
174
154
|
`;
|
|
175
|
-
|
|
176
155
|
/**
|
|
177
156
|
* Write the instructions documentation to tmp directory
|
|
178
157
|
*/
|
|
179
|
-
function writeTmpInstructions(workspaceRoot
|
|
158
|
+
function writeTmpInstructions(workspaceRoot) {
|
|
180
159
|
const tmpDir = path.join(workspaceRoot, TMP_DIR);
|
|
181
160
|
const mdPath = path.join(tmpDir, TMP_MD_FILE);
|
|
182
|
-
|
|
183
161
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
184
162
|
fs.writeFileSync(mdPath, METHODSIZE_DOC_CONTENT);
|
|
185
|
-
|
|
186
163
|
return mdPath;
|
|
187
164
|
}
|
|
188
|
-
|
|
189
165
|
/**
|
|
190
166
|
* Get changed TypeScript files between base and head (or working tree if head not specified).
|
|
191
167
|
* Uses `git diff base [head]` to match what `nx affected` does.
|
|
192
168
|
* When head is NOT specified, also includes untracked files (matching nx affected behavior).
|
|
193
169
|
*/
|
|
194
|
-
function getChangedTypeScriptFiles(workspaceRoot
|
|
170
|
+
function getChangedTypeScriptFiles(workspaceRoot, base, head) {
|
|
195
171
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
196
172
|
try {
|
|
197
173
|
// If head is specified, diff base to head; otherwise diff base to working tree
|
|
198
174
|
const diffTarget = head ? `${base} ${head}` : base;
|
|
199
|
-
const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
|
|
175
|
+
const output = (0, child_process_1.execSync)(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
|
|
200
176
|
cwd: workspaceRoot,
|
|
201
177
|
encoding: 'utf-8',
|
|
202
178
|
});
|
|
@@ -204,13 +180,12 @@ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: s
|
|
|
204
180
|
.trim()
|
|
205
181
|
.split('\n')
|
|
206
182
|
.filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
|
|
207
|
-
|
|
208
183
|
// When comparing to working tree (no head specified), also include untracked files
|
|
209
184
|
// This matches what nx affected does: "All modified files not yet committed or tracked will also be added"
|
|
210
185
|
if (!head) {
|
|
211
186
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
212
187
|
try {
|
|
213
|
-
const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
|
|
188
|
+
const untrackedOutput = (0, child_process_1.execSync)(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
|
|
214
189
|
cwd: workspaceRoot,
|
|
215
190
|
encoding: 'utf-8',
|
|
216
191
|
});
|
|
@@ -221,45 +196,43 @@ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: s
|
|
|
221
196
|
// Merge and dedupe
|
|
222
197
|
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
223
198
|
return Array.from(allFiles);
|
|
224
|
-
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
225
201
|
//const error = toError(err);
|
|
226
202
|
// If ls-files fails, just return the changed files
|
|
227
203
|
return changedFiles;
|
|
228
204
|
}
|
|
229
205
|
}
|
|
230
|
-
|
|
231
206
|
return changedFiles;
|
|
232
|
-
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
233
209
|
//const error = toError(err);
|
|
234
210
|
return [];
|
|
235
211
|
}
|
|
236
212
|
}
|
|
237
|
-
|
|
238
213
|
/**
|
|
239
214
|
* Get the diff content for a specific file between base and head (or working tree if head not specified).
|
|
240
215
|
* Uses `git diff base [head]` to match what `nx affected` does.
|
|
241
216
|
* For untracked files, returns the entire file content as additions.
|
|
242
217
|
*/
|
|
243
|
-
function getFileDiff(workspaceRoot
|
|
218
|
+
function getFileDiff(workspaceRoot, file, base, head) {
|
|
244
219
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
245
220
|
try {
|
|
246
221
|
// If head is specified, diff base to head; otherwise diff base to working tree
|
|
247
222
|
const diffTarget = head ? `${base} ${head}` : base;
|
|
248
|
-
const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
|
|
223
|
+
const diff = (0, child_process_1.execSync)(`git diff ${diffTarget} -- "${file}"`, {
|
|
249
224
|
cwd: workspaceRoot,
|
|
250
225
|
encoding: 'utf-8',
|
|
251
226
|
});
|
|
252
|
-
|
|
253
227
|
// If diff is empty and we're comparing to working tree, check if it's an untracked file
|
|
254
228
|
if (!diff && !head) {
|
|
255
229
|
const fullPath = path.join(workspaceRoot, file);
|
|
256
230
|
if (fs.existsSync(fullPath)) {
|
|
257
231
|
// Check if file is untracked
|
|
258
|
-
const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
|
|
232
|
+
const isUntracked = (0, child_process_1.execSync)(`git ls-files --others --exclude-standard "${file}"`, {
|
|
259
233
|
cwd: workspaceRoot,
|
|
260
234
|
encoding: 'utf-8',
|
|
261
235
|
}).trim();
|
|
262
|
-
|
|
263
236
|
if (isUntracked) {
|
|
264
237
|
// For untracked files, treat entire content as additions
|
|
265
238
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -269,23 +242,21 @@ function getFileDiff(workspaceRoot: string, file: string, base: string, head?: s
|
|
|
269
242
|
}
|
|
270
243
|
}
|
|
271
244
|
}
|
|
272
|
-
|
|
273
245
|
return diff;
|
|
274
|
-
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
275
248
|
//const error = toError(err);
|
|
276
249
|
return '';
|
|
277
250
|
}
|
|
278
251
|
}
|
|
279
|
-
|
|
280
252
|
/**
|
|
281
253
|
* Parse diff to find NEW method signatures.
|
|
282
254
|
* Must handle: export function, async function, const/let arrow functions, class methods
|
|
283
255
|
*/
|
|
284
256
|
// webpieces-disable max-lines-new-methods -- Regex patterns require inline documentation
|
|
285
|
-
function findNewMethodSignaturesInDiff(diffContent
|
|
286
|
-
const newMethods = new Set
|
|
257
|
+
function findNewMethodSignaturesInDiff(diffContent) {
|
|
258
|
+
const newMethods = new Set();
|
|
287
259
|
const lines = diffContent.split('\n');
|
|
288
|
-
|
|
289
260
|
// Patterns to match method definitions (same as validate-new-methods)
|
|
290
261
|
const patterns = [
|
|
291
262
|
// [export] [async] function methodName( - most explicit, check first
|
|
@@ -297,7 +268,6 @@ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
|
|
|
297
268
|
// class method: [public/private/protected] [static] [async] methodName( - but NOT constructor, if, for, while, etc.
|
|
298
269
|
/^\+\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(/,
|
|
299
270
|
];
|
|
300
|
-
|
|
301
271
|
for (const line of lines) {
|
|
302
272
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
303
273
|
for (const pattern of patterns) {
|
|
@@ -313,19 +283,15 @@ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
|
|
|
313
283
|
}
|
|
314
284
|
}
|
|
315
285
|
}
|
|
316
|
-
|
|
317
286
|
return newMethods;
|
|
318
287
|
}
|
|
319
|
-
|
|
320
288
|
/**
|
|
321
289
|
* Parse diff to find line numbers that have changes in the new file
|
|
322
290
|
*/
|
|
323
|
-
function getChangedLineNumbers(diffContent
|
|
324
|
-
const changedLines = new Set
|
|
291
|
+
function getChangedLineNumbers(diffContent) {
|
|
292
|
+
const changedLines = new Set();
|
|
325
293
|
const lines = diffContent.split('\n');
|
|
326
|
-
|
|
327
294
|
let currentNewLine = 0;
|
|
328
|
-
|
|
329
295
|
for (const line of lines) {
|
|
330
296
|
// Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
331
297
|
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
@@ -333,73 +299,60 @@ function getChangedLineNumbers(diffContent: string): Set<number> {
|
|
|
333
299
|
currentNewLine = parseInt(hunkMatch[1], 10);
|
|
334
300
|
continue;
|
|
335
301
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
302
|
+
if (currentNewLine === 0)
|
|
303
|
+
continue;
|
|
339
304
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
340
305
|
// Added line
|
|
341
306
|
changedLines.add(currentNewLine);
|
|
342
307
|
currentNewLine++;
|
|
343
|
-
}
|
|
308
|
+
}
|
|
309
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
344
310
|
// Removed line - doesn't increment new line counter
|
|
345
|
-
}
|
|
311
|
+
}
|
|
312
|
+
else if (!line.startsWith('\\')) {
|
|
346
313
|
// Context line (unchanged)
|
|
347
314
|
currentNewLine++;
|
|
348
315
|
}
|
|
349
316
|
}
|
|
350
|
-
|
|
351
317
|
return changedLines;
|
|
352
318
|
}
|
|
353
|
-
|
|
354
319
|
/**
|
|
355
320
|
* Parse a date string in yyyy/mm/dd format and return a Date object.
|
|
356
321
|
* Returns null if the format is invalid.
|
|
357
322
|
*/
|
|
358
|
-
function parseDisableDate(dateStr
|
|
323
|
+
function parseDisableDate(dateStr) {
|
|
359
324
|
// Match yyyy/mm/dd format
|
|
360
325
|
const match = dateStr.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
|
|
361
|
-
if (!match)
|
|
362
|
-
|
|
326
|
+
if (!match)
|
|
327
|
+
return null;
|
|
363
328
|
const year = parseInt(match[1], 10);
|
|
364
329
|
const month = parseInt(match[2], 10) - 1; // JS months are 0-indexed
|
|
365
330
|
const day = parseInt(match[3], 10);
|
|
366
|
-
|
|
367
331
|
const date = new Date(year, month, day);
|
|
368
|
-
|
|
369
332
|
// Validate the date is valid (e.g., not Feb 30)
|
|
370
333
|
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
371
334
|
return null;
|
|
372
335
|
}
|
|
373
|
-
|
|
374
336
|
return date;
|
|
375
337
|
}
|
|
376
|
-
|
|
377
338
|
/**
|
|
378
339
|
* Check if a date is within the last month (not expired).
|
|
379
340
|
*/
|
|
380
|
-
function isDateWithinMonth(date
|
|
341
|
+
function isDateWithinMonth(date) {
|
|
381
342
|
const now = new Date();
|
|
382
343
|
const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
|
383
344
|
return date >= oneMonthAgo;
|
|
384
345
|
}
|
|
385
|
-
|
|
386
346
|
/**
|
|
387
347
|
* Get today's date in yyyy/mm/dd format for error messages
|
|
388
348
|
*/
|
|
389
|
-
function getTodayDateString()
|
|
349
|
+
function getTodayDateString() {
|
|
390
350
|
const now = new Date();
|
|
391
351
|
const year = now.getFullYear();
|
|
392
352
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
393
353
|
const day = String(now.getDate()).padStart(2, '0');
|
|
394
354
|
return `${year}/${month}/${day}`;
|
|
395
355
|
}
|
|
396
|
-
|
|
397
|
-
interface DisableInfo {
|
|
398
|
-
type: 'full' | 'new-only' | 'none';
|
|
399
|
-
isExpired: boolean;
|
|
400
|
-
date?: string;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
356
|
/**
|
|
404
357
|
* Check what kind of webpieces-disable comment is present for a method.
|
|
405
358
|
* Returns: DisableInfo with type, expiration status, and date
|
|
@@ -408,7 +361,7 @@ interface DisableInfo {
|
|
|
408
361
|
* - 'none': no escape hatch
|
|
409
362
|
*/
|
|
410
363
|
// webpieces-disable max-lines-new-methods -- Complex validation logic with multiple escape hatch types
|
|
411
|
-
function getDisableInfo(lines
|
|
364
|
+
function getDisableInfo(lines, lineNumber) {
|
|
412
365
|
const startCheck = Math.max(0, lineNumber - 5);
|
|
413
366
|
for (let i = lineNumber - 2; i >= startCheck; i--) {
|
|
414
367
|
const line = lines[i]?.trim() ?? '';
|
|
@@ -419,60 +372,48 @@ function getDisableInfo(lines: string[], lineNumber: number): DisableInfo {
|
|
|
419
372
|
if (line.includes('max-lines-modified')) {
|
|
420
373
|
// Check for date in format: max-lines-modified yyyy/mm/dd
|
|
421
374
|
const dateMatch = line.match(/max-lines-modified\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
|
|
422
|
-
|
|
423
375
|
if (!dateMatch) {
|
|
424
376
|
// No date found - treat as expired (invalid)
|
|
425
377
|
return { type: 'full', isExpired: true, date: undefined };
|
|
426
378
|
}
|
|
427
|
-
|
|
428
379
|
const dateStr = dateMatch[1];
|
|
429
|
-
|
|
430
380
|
// Secret permanent disable
|
|
431
381
|
if (dateStr === 'XXXX/XX/XX') {
|
|
432
382
|
return { type: 'full', isExpired: false, date: dateStr };
|
|
433
383
|
}
|
|
434
|
-
|
|
435
384
|
const date = parseDisableDate(dateStr);
|
|
436
385
|
if (!date) {
|
|
437
386
|
// Invalid date format - treat as expired
|
|
438
387
|
return { type: 'full', isExpired: true, date: dateStr };
|
|
439
388
|
}
|
|
440
|
-
|
|
441
389
|
if (!isDateWithinMonth(date)) {
|
|
442
390
|
// Date is expired (older than 1 month)
|
|
443
391
|
return { type: 'full', isExpired: true, date: dateStr };
|
|
444
392
|
}
|
|
445
|
-
|
|
446
393
|
// Valid and not expired
|
|
447
394
|
return { type: 'full', isExpired: false, date: dateStr };
|
|
448
395
|
}
|
|
449
396
|
if (line.includes('max-lines-new-methods')) {
|
|
450
397
|
// Check for date in format: max-lines-new-methods yyyy/mm/dd
|
|
451
398
|
const dateMatch = line.match(/max-lines-new-methods\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
|
|
452
|
-
|
|
453
399
|
if (!dateMatch) {
|
|
454
400
|
// No date found - treat as expired (invalid)
|
|
455
401
|
return { type: 'new-only', isExpired: true, date: undefined };
|
|
456
402
|
}
|
|
457
|
-
|
|
458
403
|
const dateStr = dateMatch[1];
|
|
459
|
-
|
|
460
404
|
// Secret permanent disable
|
|
461
405
|
if (dateStr === 'XXXX/XX/XX') {
|
|
462
406
|
return { type: 'new-only', isExpired: false, date: dateStr };
|
|
463
407
|
}
|
|
464
|
-
|
|
465
408
|
const date = parseDisableDate(dateStr);
|
|
466
409
|
if (!date) {
|
|
467
410
|
// Invalid date format - treat as expired
|
|
468
411
|
return { type: 'new-only', isExpired: true, date: dateStr };
|
|
469
412
|
}
|
|
470
|
-
|
|
471
413
|
if (!isDateWithinMonth(date)) {
|
|
472
414
|
// Date is expired (older than 1 month)
|
|
473
415
|
return { type: 'new-only', isExpired: true, date: dateStr };
|
|
474
416
|
}
|
|
475
|
-
|
|
476
417
|
// Valid and not expired
|
|
477
418
|
return { type: 'new-only', isExpired: false, date: dateStr };
|
|
478
419
|
}
|
|
@@ -480,48 +421,38 @@ function getDisableInfo(lines: string[], lineNumber: number): DisableInfo {
|
|
|
480
421
|
}
|
|
481
422
|
return { type: 'none', isExpired: false };
|
|
482
423
|
}
|
|
483
|
-
|
|
484
|
-
interface MethodInfo {
|
|
485
|
-
name: string;
|
|
486
|
-
line: number;
|
|
487
|
-
endLine: number;
|
|
488
|
-
lines: number;
|
|
489
|
-
disableInfo: DisableInfo;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
424
|
/**
|
|
493
425
|
* Parse a TypeScript file and find methods with their line counts
|
|
494
426
|
*/
|
|
495
427
|
// webpieces-disable max-lines-new-methods -- AST traversal requires inline visitor function
|
|
496
|
-
function findMethodsInFile(filePath
|
|
428
|
+
function findMethodsInFile(filePath, workspaceRoot) {
|
|
497
429
|
const fullPath = path.join(workspaceRoot, filePath);
|
|
498
|
-
if (!fs.existsSync(fullPath))
|
|
499
|
-
|
|
430
|
+
if (!fs.existsSync(fullPath))
|
|
431
|
+
return [];
|
|
500
432
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
501
433
|
const fileLines = content.split('\n');
|
|
502
434
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
503
|
-
|
|
504
|
-
const methods: MethodInfo[] = [];
|
|
505
|
-
|
|
435
|
+
const methods = [];
|
|
506
436
|
// webpieces-disable max-lines-new-methods -- AST visitor pattern requires handling multiple node types
|
|
507
|
-
function visit(node
|
|
508
|
-
let methodName
|
|
509
|
-
let startLine
|
|
510
|
-
let endLine
|
|
511
|
-
|
|
437
|
+
function visit(node) {
|
|
438
|
+
let methodName;
|
|
439
|
+
let startLine;
|
|
440
|
+
let endLine;
|
|
512
441
|
if (ts.isMethodDeclaration(node) && node.name) {
|
|
513
442
|
methodName = node.name.getText(sourceFile);
|
|
514
443
|
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
515
444
|
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
516
445
|
startLine = start.line + 1;
|
|
517
446
|
endLine = end.line + 1;
|
|
518
|
-
}
|
|
447
|
+
}
|
|
448
|
+
else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
519
449
|
methodName = node.name.getText(sourceFile);
|
|
520
450
|
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
521
451
|
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
522
452
|
startLine = start.line + 1;
|
|
523
453
|
endLine = end.line + 1;
|
|
524
|
-
}
|
|
454
|
+
}
|
|
455
|
+
else if (ts.isArrowFunction(node)) {
|
|
525
456
|
if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
|
|
526
457
|
methodName = node.parent.name.getText(sourceFile);
|
|
527
458
|
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
@@ -530,7 +461,6 @@ function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[
|
|
|
530
461
|
endLine = end.line + 1;
|
|
531
462
|
}
|
|
532
463
|
}
|
|
533
|
-
|
|
534
464
|
if (methodName && startLine !== undefined && endLine !== undefined) {
|
|
535
465
|
methods.push({
|
|
536
466
|
name: methodName,
|
|
@@ -540,57 +470,51 @@ function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[
|
|
|
540
470
|
disableInfo: getDisableInfo(fileLines, startLine),
|
|
541
471
|
});
|
|
542
472
|
}
|
|
543
|
-
|
|
544
473
|
ts.forEachChild(node, visit);
|
|
545
474
|
}
|
|
546
|
-
|
|
547
475
|
visit(sourceFile);
|
|
548
476
|
return methods;
|
|
549
477
|
}
|
|
550
|
-
|
|
551
478
|
/**
|
|
552
479
|
* Check if a method has any changes within its line range
|
|
553
480
|
*/
|
|
554
|
-
function methodHasChanges(method
|
|
481
|
+
function methodHasChanges(method, changedLineNumbers) {
|
|
555
482
|
for (let line = method.line; line <= method.endLine; line++) {
|
|
556
|
-
if (changedLineNumbers.has(line))
|
|
483
|
+
if (changedLineNumbers.has(line))
|
|
484
|
+
return true;
|
|
557
485
|
}
|
|
558
486
|
return false;
|
|
559
487
|
}
|
|
560
|
-
|
|
561
488
|
/**
|
|
562
489
|
* Check a NEW method and return violation if applicable
|
|
563
490
|
*/
|
|
564
|
-
function checkNewMethodViolation(file
|
|
491
|
+
function checkNewMethodViolation(file, method, disableAllowed) {
|
|
565
492
|
const disableType = method.disableInfo.type;
|
|
566
493
|
const isExpired = method.disableInfo.isExpired;
|
|
567
494
|
const disableDate = method.disableInfo.date;
|
|
568
|
-
|
|
569
495
|
if (!disableAllowed) {
|
|
570
496
|
// When disableAllowed is false, skip NEW methods without escape (let validate-new-methods handle)
|
|
571
|
-
if (disableType === 'none')
|
|
497
|
+
if (disableType === 'none')
|
|
498
|
+
return null;
|
|
572
499
|
return { file, methodName: method.name, line: method.line, lines: method.lines };
|
|
573
500
|
}
|
|
574
|
-
|
|
575
501
|
if (disableType === 'full' && isExpired) {
|
|
576
502
|
return { file, methodName: method.name, line: method.line, lines: method.lines, expiredDisable: true, expiredDate: disableDate };
|
|
577
503
|
}
|
|
578
|
-
if (disableType !== 'new-only')
|
|
579
|
-
|
|
504
|
+
if (disableType !== 'new-only')
|
|
505
|
+
return null;
|
|
580
506
|
if (isExpired) {
|
|
581
507
|
return { file, methodName: method.name, line: method.line, lines: method.lines, expiredDisable: true, expiredDate: disableDate };
|
|
582
508
|
}
|
|
583
509
|
return { file, methodName: method.name, line: method.line, lines: method.lines };
|
|
584
510
|
}
|
|
585
|
-
|
|
586
511
|
/**
|
|
587
512
|
* Check a MODIFIED method and return violation if applicable
|
|
588
513
|
*/
|
|
589
|
-
function checkModifiedMethodViolation(file
|
|
514
|
+
function checkModifiedMethodViolation(file, method, disableAllowed) {
|
|
590
515
|
const disableType = method.disableInfo.type;
|
|
591
516
|
const isExpired = method.disableInfo.isExpired;
|
|
592
517
|
const disableDate = method.disableInfo.date;
|
|
593
|
-
|
|
594
518
|
if (!disableAllowed) {
|
|
595
519
|
return { file, methodName: method.name, line: method.line, lines: method.lines };
|
|
596
520
|
}
|
|
@@ -603,95 +527,84 @@ function checkModifiedMethodViolation(file: string, method: MethodInfo, disableA
|
|
|
603
527
|
}
|
|
604
528
|
return { file, methodName: method.name, line: method.line, lines: method.lines };
|
|
605
529
|
}
|
|
606
|
-
|
|
607
530
|
/**
|
|
608
531
|
* Find methods that exceed the limit.
|
|
609
532
|
* Checks NEW methods with escape hatches and MODIFIED methods.
|
|
610
533
|
*/
|
|
611
|
-
function findViolations(
|
|
612
|
-
|
|
613
|
-
changedFiles: string[],
|
|
614
|
-
base: string,
|
|
615
|
-
limit: number,
|
|
616
|
-
disableAllowed: boolean,
|
|
617
|
-
head?: string
|
|
618
|
-
): MethodViolation[] {
|
|
619
|
-
const violations: MethodViolation[] = [];
|
|
620
|
-
|
|
534
|
+
function findViolations(workspaceRoot, changedFiles, base, limit, disableAllowed, head) {
|
|
535
|
+
const violations = [];
|
|
621
536
|
for (const file of changedFiles) {
|
|
622
537
|
const diff = getFileDiff(workspaceRoot, file, base, head);
|
|
623
|
-
if (!diff)
|
|
624
|
-
|
|
538
|
+
if (!diff)
|
|
539
|
+
continue;
|
|
625
540
|
const newMethodNames = findNewMethodSignaturesInDiff(diff);
|
|
626
541
|
const changedLineNumbers = getChangedLineNumbers(diff);
|
|
627
|
-
if (changedLineNumbers.size === 0)
|
|
628
|
-
|
|
542
|
+
if (changedLineNumbers.size === 0)
|
|
543
|
+
continue;
|
|
629
544
|
const methods = findMethodsInFile(file, workspaceRoot);
|
|
630
|
-
|
|
631
545
|
for (const method of methods) {
|
|
632
546
|
const disableType = method.disableInfo.type;
|
|
633
547
|
const isExpired = method.disableInfo.isExpired;
|
|
634
|
-
|
|
635
548
|
// Skip methods with valid, non-expired full escape - unless disableAllowed is false
|
|
636
|
-
if (disableAllowed && disableType === 'full' && !isExpired)
|
|
637
|
-
|
|
638
|
-
|
|
549
|
+
if (disableAllowed && disableType === 'full' && !isExpired)
|
|
550
|
+
continue;
|
|
551
|
+
if (method.lines <= limit)
|
|
552
|
+
continue;
|
|
639
553
|
const isNewMethod = newMethodNames.has(method.name);
|
|
640
|
-
|
|
641
554
|
if (isNewMethod) {
|
|
642
555
|
const violation = checkNewMethodViolation(file, method, disableAllowed);
|
|
643
|
-
if (violation)
|
|
644
|
-
|
|
556
|
+
if (violation)
|
|
557
|
+
violations.push(violation);
|
|
558
|
+
}
|
|
559
|
+
else if (methodHasChanges(method, changedLineNumbers)) {
|
|
645
560
|
const violation = checkModifiedMethodViolation(file, method, disableAllowed);
|
|
646
|
-
if (violation)
|
|
561
|
+
if (violation)
|
|
562
|
+
violations.push(violation);
|
|
647
563
|
}
|
|
648
564
|
}
|
|
649
565
|
}
|
|
650
|
-
|
|
651
566
|
return violations;
|
|
652
567
|
}
|
|
653
|
-
|
|
654
568
|
/**
|
|
655
569
|
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
656
570
|
*/
|
|
657
|
-
function detectBase(workspaceRoot
|
|
571
|
+
function detectBase(workspaceRoot) {
|
|
658
572
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
659
573
|
try {
|
|
660
|
-
const mergeBase = execSync('git merge-base HEAD origin/main', {
|
|
574
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD origin/main', {
|
|
661
575
|
cwd: workspaceRoot,
|
|
662
576
|
encoding: 'utf-8',
|
|
663
577
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
664
578
|
}).trim();
|
|
665
|
-
|
|
666
579
|
if (mergeBase) {
|
|
667
580
|
return mergeBase;
|
|
668
581
|
}
|
|
669
|
-
}
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
670
584
|
//const error = toError(err);
|
|
671
585
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
672
586
|
try {
|
|
673
|
-
const mergeBase = execSync('git merge-base HEAD main', {
|
|
587
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD main', {
|
|
674
588
|
cwd: workspaceRoot,
|
|
675
589
|
encoding: 'utf-8',
|
|
676
590
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
677
591
|
}).trim();
|
|
678
|
-
|
|
679
592
|
if (mergeBase) {
|
|
680
593
|
return mergeBase;
|
|
681
594
|
}
|
|
682
|
-
}
|
|
595
|
+
}
|
|
596
|
+
catch (err2) {
|
|
683
597
|
//const error2 = toError(err2);
|
|
684
598
|
// Ignore
|
|
685
599
|
}
|
|
686
600
|
}
|
|
687
601
|
return null;
|
|
688
602
|
}
|
|
689
|
-
|
|
690
603
|
/**
|
|
691
604
|
* Report violations to console
|
|
692
605
|
*/
|
|
693
606
|
// webpieces-disable max-lines-new-methods -- Error output formatting with multiple message sections
|
|
694
|
-
function reportViolations(violations
|
|
607
|
+
function reportViolations(violations, limit, disableAllowed) {
|
|
695
608
|
console.error('');
|
|
696
609
|
console.error('\u274c Modified methods exceed ' + limit + ' lines!');
|
|
697
610
|
console.error('');
|
|
@@ -701,24 +614,21 @@ function reportViolations(violations: MethodViolation[], limit: number, disableA
|
|
|
701
614
|
console.error(' (this is for vibe coding and AI to fix as it touches things).');
|
|
702
615
|
console.error(' You can refactor to stay under the limit 50% of the time. If not feasible, use the escape hatch.');
|
|
703
616
|
console.error('');
|
|
704
|
-
console.error(
|
|
705
|
-
'\u26a0\ufe0f *** READ .webpieces/instruct-ai/webpieces.methodsize.md for detailed guidance on how to fix this easily *** \u26a0\ufe0f'
|
|
706
|
-
);
|
|
617
|
+
console.error('\u26a0\ufe0f *** READ .webpieces/instruct-ai/webpieces.methodsize.md for detailed guidance on how to fix this easily *** \u26a0\ufe0f');
|
|
707
618
|
console.error('');
|
|
708
|
-
|
|
709
619
|
for (const v of violations) {
|
|
710
620
|
if (v.expiredDisable) {
|
|
711
621
|
console.error(` \u274c ${v.file}:${v.line}`);
|
|
712
622
|
console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${limit})`);
|
|
713
623
|
console.error(` \u23f0 EXPIRED DISABLE: Your disable comment dated ${v.expiredDate ?? 'unknown'} has expired (>1 month old).`);
|
|
714
624
|
console.error(` You must either FIX the method or UPDATE the date to get another month.`);
|
|
715
|
-
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
716
627
|
console.error(` \u274c ${v.file}:${v.line}`);
|
|
717
628
|
console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${limit})`);
|
|
718
629
|
}
|
|
719
630
|
}
|
|
720
631
|
console.error('');
|
|
721
|
-
|
|
722
632
|
// Only show escape hatch instructions when disableAllowed is true
|
|
723
633
|
if (disableAllowed) {
|
|
724
634
|
console.error(' You can disable this error, but you will be forced to fix again in 1 month');
|
|
@@ -727,7 +637,8 @@ function reportViolations(violations: MethodViolation[], limit: number, disableA
|
|
|
727
637
|
console.error(' Use escape with DATE (expires in 1 month):');
|
|
728
638
|
console.error(` // webpieces-disable max-lines-modified ${getTodayDateString()} -- [your reason]`);
|
|
729
639
|
console.error('');
|
|
730
|
-
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
731
642
|
console.error(' \u26a0\ufe0f disableAllowed is false - disable comments are NOT allowed.');
|
|
732
643
|
console.error(' This rule must be met and cannot be disabled since nx.json disableAllowed is set to false.');
|
|
733
644
|
console.error(' You MUST refactor to reduce method size.');
|
|
@@ -740,73 +651,60 @@ function reportViolations(violations: MethodViolation[], limit: number, disableA
|
|
|
740
651
|
console.error('');
|
|
741
652
|
}
|
|
742
653
|
}
|
|
743
|
-
|
|
744
|
-
export default async function runValidator(
|
|
745
|
-
options: ValidateModifiedMethodsOptions,
|
|
746
|
-
workspaceRoot: string
|
|
747
|
-
): Promise<ExecutorResult> {
|
|
654
|
+
async function runValidator(options, workspaceRoot) {
|
|
748
655
|
const limit = options.limit ?? 80;
|
|
749
|
-
const mode
|
|
656
|
+
const mode = options.mode ?? 'NEW_AND_MODIFIED_METHODS';
|
|
750
657
|
const disableAllowed = options.disableAllowed ?? true;
|
|
751
|
-
|
|
752
658
|
// Skip validation entirely if mode is OFF
|
|
753
659
|
if (mode === 'OFF') {
|
|
754
660
|
console.log('\n\u23ed\ufe0f Skipping modified method validation (mode: OFF)');
|
|
755
661
|
console.log('');
|
|
756
662
|
return { success: true };
|
|
757
663
|
}
|
|
758
|
-
|
|
759
664
|
// If NX_HEAD is set (via nx affected --head=X), use it; otherwise compare to working tree
|
|
760
665
|
let base = process.env['NX_BASE'];
|
|
761
666
|
const head = process.env['NX_HEAD'];
|
|
762
|
-
|
|
763
667
|
if (!base) {
|
|
764
668
|
base = detectBase(workspaceRoot) ?? undefined;
|
|
765
|
-
|
|
766
669
|
if (!base) {
|
|
767
670
|
console.log('\n\u23ed\ufe0f Skipping modified method validation (could not detect base branch)');
|
|
768
671
|
console.log(' To run explicitly: nx affected --target=validate-modified-methods --base=origin/main');
|
|
769
672
|
console.log('');
|
|
770
673
|
return { success: true };
|
|
771
674
|
}
|
|
772
|
-
|
|
773
675
|
console.log('\n\ud83d\udccf Validating Modified Method Sizes (auto-detected base)\n');
|
|
774
|
-
}
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
775
678
|
console.log('\n\ud83d\udccf Validating Modified Method Sizes\n');
|
|
776
679
|
}
|
|
777
|
-
|
|
778
680
|
console.log(` Base: ${base}`);
|
|
779
681
|
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
780
682
|
console.log(` Mode: ${mode}`);
|
|
781
683
|
console.log(` Limit for modified methods: ${limit}`);
|
|
782
684
|
console.log(` Disable allowed: ${disableAllowed}${!disableAllowed ? ' (no escape hatch)' : ''}`);
|
|
783
685
|
console.log('');
|
|
784
|
-
|
|
785
686
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
786
687
|
try {
|
|
787
688
|
const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
|
|
788
|
-
|
|
789
689
|
if (changedFiles.length === 0) {
|
|
790
690
|
console.log('\u2705 No TypeScript files changed');
|
|
791
691
|
return { success: true };
|
|
792
692
|
}
|
|
793
|
-
|
|
794
693
|
console.log(`\ud83d\udcc2 Checking ${changedFiles.length} changed file(s)...`);
|
|
795
|
-
|
|
796
694
|
const violations = findViolations(workspaceRoot, changedFiles, base, limit, disableAllowed, head);
|
|
797
|
-
|
|
798
695
|
if (violations.length === 0) {
|
|
799
696
|
console.log('\u2705 All modified methods are under ' + limit + ' lines');
|
|
800
697
|
return { success: true };
|
|
801
698
|
}
|
|
802
|
-
|
|
803
699
|
writeTmpInstructions(workspaceRoot);
|
|
804
700
|
reportViolations(violations, limit, disableAllowed);
|
|
805
701
|
return { success: false };
|
|
806
|
-
}
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
807
704
|
//const error = toError(err);
|
|
808
705
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
809
706
|
console.error('\u274c Modified method validation failed:', error.message);
|
|
810
707
|
return { success: false };
|
|
811
708
|
}
|
|
812
709
|
}
|
|
710
|
+
//# sourceMappingURL=validate-modified-methods.js.map
|