@webpieces/code-rules 0.0.1 → 0.2.113
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 DTOs Executor
|
|
3
4
|
*
|
|
@@ -27,99 +28,57 @@
|
|
|
27
28
|
* - Dto fields must be a subset of Dbo fields
|
|
28
29
|
* - Extra Dbo fields are allowed (e.g., password)
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export interface ValidateDtosOptions {
|
|
39
|
-
mode?: ValidateDtosMode;
|
|
40
|
-
disableAllowed?: boolean;
|
|
41
|
-
prismaSchemaPath?: string;
|
|
42
|
-
dtoSourcePaths?: string[];
|
|
43
|
-
ignoreModifiedUntilEpoch?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ExecutorResult {
|
|
47
|
-
success: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface DtoFieldInfo {
|
|
51
|
-
name: string;
|
|
52
|
-
line: number;
|
|
53
|
-
deprecated: boolean;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface DtoInfo {
|
|
57
|
-
name: string;
|
|
58
|
-
file: string;
|
|
59
|
-
startLine: number;
|
|
60
|
-
endLine: number;
|
|
61
|
-
fields: DtoFieldInfo[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface DtoViolation {
|
|
65
|
-
file: string;
|
|
66
|
-
line: number;
|
|
67
|
-
dtoName: string;
|
|
68
|
-
fieldName: string;
|
|
69
|
-
dboName: string;
|
|
70
|
-
availableFields: string[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface DboEntry {
|
|
74
|
-
name: string;
|
|
75
|
-
fields: Set<string>;
|
|
76
|
-
}
|
|
77
|
-
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.default = runValidator;
|
|
33
|
+
const tslib_1 = require("tslib");
|
|
34
|
+
const child_process_1 = require("child_process");
|
|
35
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
36
|
+
const path = tslib_1.__importStar(require("path"));
|
|
37
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
78
38
|
/**
|
|
79
39
|
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
80
40
|
*/
|
|
81
|
-
function detectBase(workspaceRoot
|
|
41
|
+
function detectBase(workspaceRoot) {
|
|
82
42
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
83
43
|
try {
|
|
84
|
-
const mergeBase = execSync('git merge-base HEAD origin/main', {
|
|
44
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD origin/main', {
|
|
85
45
|
cwd: workspaceRoot,
|
|
86
46
|
encoding: 'utf-8',
|
|
87
47
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
48
|
}).trim();
|
|
89
|
-
|
|
90
49
|
if (mergeBase) {
|
|
91
50
|
return mergeBase;
|
|
92
51
|
}
|
|
93
|
-
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
94
54
|
//const error = toError(err);
|
|
95
55
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
96
56
|
try {
|
|
97
|
-
const mergeBase = execSync('git merge-base HEAD main', {
|
|
57
|
+
const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD main', {
|
|
98
58
|
cwd: workspaceRoot,
|
|
99
59
|
encoding: 'utf-8',
|
|
100
60
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
101
61
|
}).trim();
|
|
102
|
-
|
|
103
62
|
if (mergeBase) {
|
|
104
63
|
return mergeBase;
|
|
105
64
|
}
|
|
106
|
-
}
|
|
65
|
+
}
|
|
66
|
+
catch (err2) {
|
|
107
67
|
//const error2 = toError(err2);
|
|
108
68
|
// Ignore
|
|
109
69
|
}
|
|
110
70
|
}
|
|
111
71
|
return null;
|
|
112
72
|
}
|
|
113
|
-
|
|
114
73
|
/**
|
|
115
74
|
* Get changed files between base and head (or working tree if head not specified).
|
|
116
75
|
*/
|
|
117
76
|
// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
|
|
118
|
-
function getChangedFiles(workspaceRoot
|
|
77
|
+
function getChangedFiles(workspaceRoot, base, head) {
|
|
119
78
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
120
79
|
try {
|
|
121
80
|
const diffTarget = head ? `${base} ${head}` : base;
|
|
122
|
-
const output = execSync(`git diff --name-only ${diffTarget}`, {
|
|
81
|
+
const output = (0, child_process_1.execSync)(`git diff --name-only ${diffTarget}`, {
|
|
123
82
|
cwd: workspaceRoot,
|
|
124
83
|
encoding: 'utf-8',
|
|
125
84
|
});
|
|
@@ -127,11 +86,10 @@ function getChangedFiles(workspaceRoot: string, base: string, head?: string): st
|
|
|
127
86
|
.trim()
|
|
128
87
|
.split('\n')
|
|
129
88
|
.filter((f) => f.length > 0);
|
|
130
|
-
|
|
131
89
|
if (!head) {
|
|
132
90
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
133
91
|
try {
|
|
134
|
-
const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
|
|
92
|
+
const untrackedOutput = (0, child_process_1.execSync)('git ls-files --others --exclude-standard', {
|
|
135
93
|
cwd: workspaceRoot,
|
|
136
94
|
encoding: 'utf-8',
|
|
137
95
|
});
|
|
@@ -141,39 +99,37 @@ function getChangedFiles(workspaceRoot: string, base: string, head?: string): st
|
|
|
141
99
|
.filter((f) => f.length > 0);
|
|
142
100
|
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
143
101
|
return Array.from(allFiles);
|
|
144
|
-
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
145
104
|
//const error = toError(err);
|
|
146
105
|
return changedFiles;
|
|
147
106
|
}
|
|
148
107
|
}
|
|
149
|
-
|
|
150
108
|
return changedFiles;
|
|
151
|
-
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
152
111
|
//const error = toError(err);
|
|
153
112
|
return [];
|
|
154
113
|
}
|
|
155
114
|
}
|
|
156
|
-
|
|
157
115
|
/**
|
|
158
116
|
* Get the diff content for a specific file.
|
|
159
117
|
*/
|
|
160
|
-
function getFileDiff(workspaceRoot
|
|
118
|
+
function getFileDiff(workspaceRoot, file, base, head) {
|
|
161
119
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
162
120
|
try {
|
|
163
121
|
const diffTarget = head ? `${base} ${head}` : base;
|
|
164
|
-
const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
|
|
122
|
+
const diff = (0, child_process_1.execSync)(`git diff ${diffTarget} -- "${file}"`, {
|
|
165
123
|
cwd: workspaceRoot,
|
|
166
124
|
encoding: 'utf-8',
|
|
167
125
|
});
|
|
168
|
-
|
|
169
126
|
if (!diff && !head) {
|
|
170
127
|
const fullPath = path.join(workspaceRoot, file);
|
|
171
128
|
if (fs.existsSync(fullPath)) {
|
|
172
|
-
const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
|
|
129
|
+
const isUntracked = (0, child_process_1.execSync)(`git ls-files --others --exclude-standard "${file}"`, {
|
|
173
130
|
cwd: workspaceRoot,
|
|
174
131
|
encoding: 'utf-8',
|
|
175
132
|
}).trim();
|
|
176
|
-
|
|
177
133
|
if (isUntracked) {
|
|
178
134
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
179
135
|
const lines = content.split('\n');
|
|
@@ -181,94 +137,82 @@ function getFileDiff(workspaceRoot: string, file: string, base: string, head?: s
|
|
|
181
137
|
}
|
|
182
138
|
}
|
|
183
139
|
}
|
|
184
|
-
|
|
185
140
|
return diff;
|
|
186
|
-
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
187
143
|
//const error = toError(err);
|
|
188
144
|
return '';
|
|
189
145
|
}
|
|
190
146
|
}
|
|
191
|
-
|
|
192
147
|
/**
|
|
193
148
|
* Parse diff to extract changed line numbers (additions only - lines starting with +).
|
|
194
149
|
*/
|
|
195
|
-
function getChangedLineNumbers(diffContent
|
|
196
|
-
const changedLines = new Set
|
|
150
|
+
function getChangedLineNumbers(diffContent) {
|
|
151
|
+
const changedLines = new Set();
|
|
197
152
|
const lines = diffContent.split('\n');
|
|
198
153
|
let currentLine = 0;
|
|
199
|
-
|
|
200
154
|
for (const line of lines) {
|
|
201
155
|
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
202
156
|
if (hunkMatch) {
|
|
203
157
|
currentLine = parseInt(hunkMatch[1], 10);
|
|
204
158
|
continue;
|
|
205
159
|
}
|
|
206
|
-
|
|
207
160
|
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
208
161
|
changedLines.add(currentLine);
|
|
209
162
|
currentLine++;
|
|
210
|
-
}
|
|
163
|
+
}
|
|
164
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
211
165
|
// Deletions don't increment line number
|
|
212
|
-
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
213
168
|
currentLine++;
|
|
214
169
|
}
|
|
215
170
|
}
|
|
216
|
-
|
|
217
171
|
return changedLines;
|
|
218
172
|
}
|
|
219
|
-
|
|
220
173
|
/**
|
|
221
174
|
* Convert a snake_case string to camelCase.
|
|
222
175
|
* e.g., "version_number" -> "versionNumber", "id" -> "id"
|
|
223
176
|
*/
|
|
224
|
-
function snakeToCamel(s
|
|
225
|
-
return s.replace(/_([a-z])/g, (_, letter
|
|
177
|
+
function snakeToCamel(s) {
|
|
178
|
+
return s.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
226
179
|
}
|
|
227
|
-
|
|
228
180
|
/**
|
|
229
181
|
* Parse schema.prisma to build a map of Dbo model name -> set of field names (camelCase).
|
|
230
182
|
* Only models whose name ends with "Dbo" are included.
|
|
231
183
|
* Field names are converted from snake_case to camelCase since Dto fields use camelCase.
|
|
232
184
|
*/
|
|
233
|
-
function parsePrismaSchema(schemaPath
|
|
234
|
-
const models = new Map
|
|
235
|
-
|
|
185
|
+
function parsePrismaSchema(schemaPath) {
|
|
186
|
+
const models = new Map();
|
|
236
187
|
if (!fs.existsSync(schemaPath)) {
|
|
237
188
|
return models;
|
|
238
189
|
}
|
|
239
|
-
|
|
240
190
|
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
241
191
|
const lines = content.split('\n');
|
|
242
|
-
|
|
243
|
-
let
|
|
244
|
-
let currentFields: Set<string> | null = null;
|
|
245
|
-
|
|
192
|
+
let currentModel = null;
|
|
193
|
+
let currentFields = null;
|
|
246
194
|
for (const line of lines) {
|
|
247
195
|
const trimmed = line.trim();
|
|
248
|
-
|
|
249
196
|
// Match model declaration: model XxxDbo {
|
|
250
197
|
const modelMatch = trimmed.match(/^model\s+(\w+Dbo)\s*\{/);
|
|
251
198
|
if (modelMatch) {
|
|
252
199
|
currentModel = modelMatch[1];
|
|
253
|
-
currentFields = new Set
|
|
200
|
+
currentFields = new Set();
|
|
254
201
|
continue;
|
|
255
202
|
}
|
|
256
|
-
|
|
257
203
|
// End of model block
|
|
258
204
|
if (currentModel && trimmed === '}') {
|
|
259
|
-
models.set(currentModel, currentFields
|
|
205
|
+
models.set(currentModel, currentFields);
|
|
260
206
|
currentModel = null;
|
|
261
207
|
currentFields = null;
|
|
262
208
|
continue;
|
|
263
209
|
}
|
|
264
|
-
|
|
265
210
|
// Inside a model block - extract field names
|
|
266
211
|
if (currentModel && currentFields) {
|
|
267
212
|
// Skip empty lines, comments, and model-level attributes (@@)
|
|
268
213
|
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) {
|
|
269
214
|
continue;
|
|
270
215
|
}
|
|
271
|
-
|
|
272
216
|
// Field name is the first word on the line, converted to camelCase
|
|
273
217
|
const fieldMatch = trimmed.match(/^(\w+)\s/);
|
|
274
218
|
if (fieldMatch) {
|
|
@@ -276,50 +220,43 @@ function parsePrismaSchema(schemaPath: string): Map<string, Set<string>> {
|
|
|
276
220
|
}
|
|
277
221
|
}
|
|
278
222
|
}
|
|
279
|
-
|
|
280
223
|
return models;
|
|
281
224
|
}
|
|
282
|
-
|
|
283
225
|
/**
|
|
284
226
|
* Check if a field has @deprecated in a comment above it (within 3 lines).
|
|
285
227
|
*/
|
|
286
|
-
function isFieldDeprecated(fileLines
|
|
228
|
+
function isFieldDeprecated(fileLines, fieldLine) {
|
|
287
229
|
const start = Math.max(0, fieldLine - 4);
|
|
288
230
|
for (let i = start; i <= fieldLine - 1; i++) {
|
|
289
231
|
const line = fileLines[i]?.trim() ?? '';
|
|
290
|
-
if (line.includes('@deprecated'))
|
|
232
|
+
if (line.includes('@deprecated'))
|
|
233
|
+
return true;
|
|
291
234
|
}
|
|
292
235
|
return false;
|
|
293
236
|
}
|
|
294
|
-
|
|
295
237
|
/**
|
|
296
238
|
* Parse a TypeScript file to find Dto class/interface declarations and their fields.
|
|
297
239
|
* Skips classes ending with "JoinDto" since they compose other Dtos.
|
|
298
240
|
*/
|
|
299
241
|
// webpieces-disable max-lines-new-methods -- AST traversal for both class and interface Dto detection with field extraction
|
|
300
|
-
function findDtosInFile(filePath
|
|
242
|
+
function findDtosInFile(filePath, workspaceRoot) {
|
|
301
243
|
const fullPath = path.join(workspaceRoot, filePath);
|
|
302
|
-
if (!fs.existsSync(fullPath))
|
|
303
|
-
|
|
244
|
+
if (!fs.existsSync(fullPath))
|
|
245
|
+
return [];
|
|
304
246
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
305
247
|
const fileLines = content.split('\n');
|
|
306
248
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
function visit(node: ts.Node): void {
|
|
249
|
+
const dtos = [];
|
|
250
|
+
function visit(node) {
|
|
311
251
|
const isClass = ts.isClassDeclaration(node);
|
|
312
252
|
const isInterface = ts.isInterfaceDeclaration(node);
|
|
313
|
-
|
|
314
253
|
if ((isClass || isInterface) && node.name) {
|
|
315
254
|
const name = node.name.text;
|
|
316
|
-
|
|
317
255
|
// Must end with Dto but NOT with JoinDto
|
|
318
256
|
if (name.endsWith('Dto') && !name.endsWith('JoinDto')) {
|
|
319
|
-
const fields
|
|
257
|
+
const fields = [];
|
|
320
258
|
const nodeStart = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
321
259
|
const nodeEnd = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
322
|
-
|
|
323
260
|
for (const member of node.members) {
|
|
324
261
|
if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
|
|
325
262
|
if (member.name && ts.isIdentifier(member.name)) {
|
|
@@ -328,12 +265,10 @@ function findDtosInFile(filePath: string, workspaceRoot: string): DtoInfo[] {
|
|
|
328
265
|
const pos = sourceFile.getLineAndCharacterOfPosition(startPos);
|
|
329
266
|
const line = pos.line + 1;
|
|
330
267
|
const deprecated = isFieldDeprecated(fileLines, line);
|
|
331
|
-
|
|
332
268
|
fields.push({ name: fieldName, line, deprecated });
|
|
333
269
|
}
|
|
334
270
|
}
|
|
335
271
|
}
|
|
336
|
-
|
|
337
272
|
dtos.push({
|
|
338
273
|
name,
|
|
339
274
|
file: filePath,
|
|
@@ -343,50 +278,39 @@ function findDtosInFile(filePath: string, workspaceRoot: string): DtoInfo[] {
|
|
|
343
278
|
});
|
|
344
279
|
}
|
|
345
280
|
}
|
|
346
|
-
|
|
347
281
|
ts.forEachChild(node, visit);
|
|
348
282
|
}
|
|
349
|
-
|
|
350
283
|
visit(sourceFile);
|
|
351
284
|
return dtos;
|
|
352
285
|
}
|
|
353
|
-
|
|
354
286
|
/**
|
|
355
287
|
* Extract the prefix from a Dto/Dbo name by removing the suffix.
|
|
356
288
|
* e.g., "UserDto" -> "user", "UserDbo" -> "user"
|
|
357
289
|
*/
|
|
358
|
-
function extractPrefix(name
|
|
290
|
+
function extractPrefix(name, suffix) {
|
|
359
291
|
return name.slice(0, -suffix.length).toLowerCase();
|
|
360
292
|
}
|
|
361
|
-
|
|
362
293
|
/**
|
|
363
294
|
* Find violations: Dto fields that don't exist in the corresponding Dbo.
|
|
364
295
|
*/
|
|
365
|
-
function findViolations(
|
|
366
|
-
|
|
367
|
-
dboModels: Map<string, Set<string>>
|
|
368
|
-
): DtoViolation[] {
|
|
369
|
-
const violations: DtoViolation[] = [];
|
|
370
|
-
|
|
296
|
+
function findViolations(dtos, dboModels) {
|
|
297
|
+
const violations = [];
|
|
371
298
|
// Build a lowercase prefix -> Dbo info map
|
|
372
|
-
const dboByPrefix = new Map
|
|
299
|
+
const dboByPrefix = new Map();
|
|
373
300
|
dboModels.forEach((fields, dboName) => {
|
|
374
301
|
const prefix = extractPrefix(dboName, 'Dbo');
|
|
375
302
|
dboByPrefix.set(prefix, { name: dboName, fields });
|
|
376
303
|
});
|
|
377
|
-
|
|
378
304
|
for (const dto of dtos) {
|
|
379
305
|
const prefix = extractPrefix(dto.name, 'Dto');
|
|
380
306
|
const dbo = dboByPrefix.get(prefix);
|
|
381
|
-
|
|
382
307
|
if (!dbo) {
|
|
383
308
|
// No matching Dbo found - skip (might be a Dto without a DB table)
|
|
384
309
|
continue;
|
|
385
310
|
}
|
|
386
|
-
|
|
387
311
|
for (const field of dto.fields) {
|
|
388
|
-
if (field.deprecated)
|
|
389
|
-
|
|
312
|
+
if (field.deprecated)
|
|
313
|
+
continue;
|
|
390
314
|
if (!dbo.fields.has(field.name)) {
|
|
391
315
|
violations.push({
|
|
392
316
|
file: dto.file,
|
|
@@ -399,29 +323,27 @@ function findViolations(
|
|
|
399
323
|
}
|
|
400
324
|
}
|
|
401
325
|
}
|
|
402
|
-
|
|
403
326
|
return violations;
|
|
404
327
|
}
|
|
405
|
-
|
|
406
328
|
/**
|
|
407
329
|
* Compute similarity between two strings using longest common subsequence ratio.
|
|
408
330
|
* Returns a value between 0 and 1, where 1 is an exact match.
|
|
409
331
|
*/
|
|
410
|
-
function similarity(a
|
|
332
|
+
function similarity(a, b) {
|
|
411
333
|
const al = a.toLowerCase();
|
|
412
334
|
const bl = b.toLowerCase();
|
|
413
|
-
if (al === bl)
|
|
414
|
-
|
|
335
|
+
if (al === bl)
|
|
336
|
+
return 1;
|
|
415
337
|
const m = al.length;
|
|
416
338
|
const n = bl.length;
|
|
417
|
-
const prev = new Array
|
|
418
|
-
const curr = new Array
|
|
419
|
-
|
|
339
|
+
const prev = new Array(n + 1).fill(0);
|
|
340
|
+
const curr = new Array(n + 1).fill(0);
|
|
420
341
|
for (let i = 1; i <= m; i++) {
|
|
421
342
|
for (let j = 1; j <= n; j++) {
|
|
422
343
|
if (al[i - 1] === bl[j - 1]) {
|
|
423
344
|
curr[j] = prev[j - 1] + 1;
|
|
424
|
-
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
425
347
|
curr[j] = Math.max(prev[j], curr[j - 1]);
|
|
426
348
|
}
|
|
427
349
|
}
|
|
@@ -430,19 +352,16 @@ function similarity(a: string, b: string): number {
|
|
|
430
352
|
curr[j] = 0;
|
|
431
353
|
}
|
|
432
354
|
}
|
|
433
|
-
|
|
434
355
|
const lcsLen = prev[n];
|
|
435
356
|
return (2 * lcsLen) / (m + n);
|
|
436
357
|
}
|
|
437
|
-
|
|
438
358
|
/**
|
|
439
359
|
* Find the closest matching Dbo field for a given Dto field name.
|
|
440
360
|
* Returns the best match or null if nothing scores above the threshold.
|
|
441
361
|
*/
|
|
442
|
-
function suggestClosestField(fieldName
|
|
443
|
-
let bestMatch
|
|
362
|
+
function suggestClosestField(fieldName, availableFields) {
|
|
363
|
+
let bestMatch = null;
|
|
444
364
|
let bestScore = 0.4; // minimum threshold
|
|
445
|
-
|
|
446
365
|
for (const candidate of availableFields) {
|
|
447
366
|
const score = similarity(fieldName, candidate);
|
|
448
367
|
if (score > bestScore) {
|
|
@@ -450,14 +369,12 @@ function suggestClosestField(fieldName: string, availableFields: string[]): stri
|
|
|
450
369
|
bestMatch = candidate;
|
|
451
370
|
}
|
|
452
371
|
}
|
|
453
|
-
|
|
454
372
|
return bestMatch;
|
|
455
373
|
}
|
|
456
|
-
|
|
457
374
|
/**
|
|
458
375
|
* Report violations to console with prominent AI warning and rename suggestions.
|
|
459
376
|
*/
|
|
460
|
-
function reportViolations(violations
|
|
377
|
+
function reportViolations(violations) {
|
|
461
378
|
console.error('');
|
|
462
379
|
console.error('❌❌❌ DTO FIELD MISMATCH - AI AGENT: READ THIS CAREFULLY ❌❌❌');
|
|
463
380
|
console.error('');
|
|
@@ -473,77 +390,68 @@ function reportViolations(violations: DtoViolation[]): void {
|
|
|
473
390
|
console.error(' contains YYDto and ZZDto fields from the other tables instead of flattening.');
|
|
474
391
|
console.error(' Fields marked @deprecated in the Dto are exempt from this check.');
|
|
475
392
|
console.error('');
|
|
476
|
-
|
|
477
393
|
for (const v of violations) {
|
|
478
394
|
console.error(` ❌ ${v.file}:${v.line}`);
|
|
479
395
|
console.error(` ${v.dtoName}.${v.fieldName} does not exist in ${v.dboName}`);
|
|
480
|
-
|
|
481
396
|
const suggestion = suggestClosestField(v.fieldName, v.availableFields);
|
|
482
397
|
if (suggestion) {
|
|
483
398
|
console.error(` 💡 Suggested rename: ${v.fieldName} → ${suggestion} (Use IDE rename on ${v.dtoName}.${v.fieldName})`);
|
|
484
|
-
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
485
401
|
const preview = v.availableFields.slice(0, 8).join(', ');
|
|
486
402
|
const ellipsis = v.availableFields.length > 8 ? ', ...' : '';
|
|
487
403
|
console.error(` 💡 No close match found. Available: ${preview}${ellipsis}`);
|
|
488
404
|
}
|
|
489
405
|
}
|
|
490
406
|
console.error('');
|
|
491
|
-
|
|
492
407
|
console.error('❌❌❌ AI AGENT: Suggest to the human which are the most likely renames needed and list them ❌❌❌');
|
|
493
408
|
console.error('');
|
|
494
409
|
}
|
|
495
|
-
|
|
496
410
|
/**
|
|
497
411
|
* Filter changed files to only TypeScript Dto source files within configured paths.
|
|
498
412
|
*/
|
|
499
|
-
function filterDtoFiles(changedFiles
|
|
413
|
+
function filterDtoFiles(changedFiles, dtoSourcePaths) {
|
|
500
414
|
return changedFiles.filter((f) => {
|
|
501
|
-
if (!f.endsWith('.ts') && !f.endsWith('.tsx'))
|
|
502
|
-
|
|
415
|
+
if (!f.endsWith('.ts') && !f.endsWith('.tsx'))
|
|
416
|
+
return false;
|
|
417
|
+
if (f.includes('.spec.ts') || f.includes('.test.ts'))
|
|
418
|
+
return false;
|
|
503
419
|
return dtoSourcePaths.some((srcPath) => f.startsWith(srcPath));
|
|
504
420
|
});
|
|
505
421
|
}
|
|
506
|
-
|
|
507
422
|
/**
|
|
508
423
|
* Collect all Dto definitions from the given files.
|
|
509
424
|
*/
|
|
510
|
-
function collectDtos(dtoFiles
|
|
511
|
-
const allDtos
|
|
425
|
+
function collectDtos(dtoFiles, workspaceRoot) {
|
|
426
|
+
const allDtos = [];
|
|
512
427
|
for (const file of dtoFiles) {
|
|
513
428
|
const dtos = findDtosInFile(file, workspaceRoot);
|
|
514
429
|
allDtos.push(...dtos);
|
|
515
430
|
}
|
|
516
431
|
return allDtos;
|
|
517
432
|
}
|
|
518
|
-
|
|
519
433
|
/**
|
|
520
434
|
* Check if a Dto class overlaps with any changed lines in the diff.
|
|
521
435
|
*/
|
|
522
|
-
function isDtoTouched(dto
|
|
436
|
+
function isDtoTouched(dto, changedLines) {
|
|
523
437
|
for (let line = dto.startLine; line <= dto.endLine; line++) {
|
|
524
|
-
if (changedLines.has(line))
|
|
438
|
+
if (changedLines.has(line))
|
|
439
|
+
return true;
|
|
525
440
|
}
|
|
526
441
|
return false;
|
|
527
442
|
}
|
|
528
|
-
|
|
529
443
|
/**
|
|
530
444
|
* Filter Dtos to only those that have changed lines in the diff (MODIFIED_CLASS mode).
|
|
531
445
|
*/
|
|
532
|
-
function filterTouchedDtos(
|
|
533
|
-
dtos: DtoInfo[],
|
|
534
|
-
workspaceRoot: string,
|
|
535
|
-
base: string,
|
|
536
|
-
head?: string
|
|
537
|
-
): DtoInfo[] {
|
|
446
|
+
function filterTouchedDtos(dtos, workspaceRoot, base, head) {
|
|
538
447
|
// Group dtos by file to avoid re-fetching diffs
|
|
539
|
-
const byFile = new Map
|
|
448
|
+
const byFile = new Map();
|
|
540
449
|
for (const dto of dtos) {
|
|
541
450
|
const list = byFile.get(dto.file) ?? [];
|
|
542
451
|
list.push(dto);
|
|
543
452
|
byFile.set(dto.file, list);
|
|
544
453
|
}
|
|
545
|
-
|
|
546
|
-
const touched: DtoInfo[] = [];
|
|
454
|
+
const touched = [];
|
|
547
455
|
byFile.forEach((fileDtos, file) => {
|
|
548
456
|
const diff = getFileDiff(workspaceRoot, file, base, head);
|
|
549
457
|
const changedLines = getChangedLineNumbers(diff);
|
|
@@ -555,62 +463,44 @@ function filterTouchedDtos(
|
|
|
555
463
|
});
|
|
556
464
|
return touched;
|
|
557
465
|
}
|
|
558
|
-
|
|
559
466
|
/**
|
|
560
467
|
* Resolve git base ref from env vars or auto-detection.
|
|
561
468
|
*/
|
|
562
|
-
function resolveBase(workspaceRoot
|
|
469
|
+
function resolveBase(workspaceRoot) {
|
|
563
470
|
const envBase = process.env['NX_BASE'];
|
|
564
|
-
if (envBase)
|
|
471
|
+
if (envBase)
|
|
472
|
+
return envBase;
|
|
565
473
|
return detectBase(workspaceRoot) ?? undefined;
|
|
566
474
|
}
|
|
567
|
-
|
|
568
475
|
/**
|
|
569
476
|
* Run the core validation after early-exit checks have passed.
|
|
570
477
|
*/
|
|
571
478
|
// webpieces-disable max-lines-new-methods -- Core validation orchestration with multiple early-exit checks
|
|
572
|
-
function validateDtoFiles(
|
|
573
|
-
workspaceRoot: string,
|
|
574
|
-
prismaSchemaPath: string,
|
|
575
|
-
changedFiles: string[],
|
|
576
|
-
dtoSourcePaths: string[],
|
|
577
|
-
mode: ValidateDtosMode,
|
|
578
|
-
base: string,
|
|
579
|
-
head?: string
|
|
580
|
-
): ExecutorResult {
|
|
479
|
+
function validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths, mode, base, head) {
|
|
581
480
|
if (changedFiles.some((f) => f.endsWith(prismaSchemaPath))) {
|
|
582
481
|
console.log('⏭️ Skipping validate-dtos (schema.prisma is modified - schema in flux)');
|
|
583
482
|
console.log('');
|
|
584
483
|
return { success: true };
|
|
585
484
|
}
|
|
586
|
-
|
|
587
485
|
const dtoFiles = filterDtoFiles(changedFiles, dtoSourcePaths);
|
|
588
|
-
|
|
589
486
|
if (dtoFiles.length === 0) {
|
|
590
487
|
console.log('✅ No Dto files changed');
|
|
591
488
|
return { success: true };
|
|
592
489
|
}
|
|
593
|
-
|
|
594
490
|
console.log(`📂 Checking ${dtoFiles.length} changed file(s) for Dto definitions...`);
|
|
595
|
-
|
|
596
491
|
const fullSchemaPath = path.join(workspaceRoot, prismaSchemaPath);
|
|
597
492
|
const dboModels = parsePrismaSchema(fullSchemaPath);
|
|
598
|
-
|
|
599
493
|
if (dboModels.size === 0) {
|
|
600
494
|
console.log('⏭️ No Dbo models found in schema.prisma');
|
|
601
495
|
console.log('');
|
|
602
496
|
return { success: true };
|
|
603
497
|
}
|
|
604
|
-
|
|
605
498
|
console.log(` Found ${dboModels.size} Dbo model(s) in schema.prisma`);
|
|
606
|
-
|
|
607
499
|
let allDtos = collectDtos(dtoFiles, workspaceRoot);
|
|
608
|
-
|
|
609
500
|
if (allDtos.length === 0) {
|
|
610
501
|
console.log('✅ No Dto definitions found in changed files');
|
|
611
502
|
return { success: true };
|
|
612
503
|
}
|
|
613
|
-
|
|
614
504
|
// In MODIFIED_CLASS mode, narrow to only Dtos with changed lines
|
|
615
505
|
if (mode === 'MODIFIED_CLASS') {
|
|
616
506
|
allDtos = filterTouchedDtos(allDtos, workspaceRoot, base, head);
|
|
@@ -619,25 +509,20 @@ function validateDtoFiles(
|
|
|
619
509
|
return { success: true };
|
|
620
510
|
}
|
|
621
511
|
}
|
|
622
|
-
|
|
623
512
|
console.log(` Validating ${allDtos.length} Dto definition(s)`);
|
|
624
|
-
|
|
625
513
|
const violations = findViolations(allDtos, dboModels);
|
|
626
|
-
|
|
627
514
|
if (violations.length === 0) {
|
|
628
515
|
console.log('✅ All Dto fields match their Dbo models');
|
|
629
516
|
return { success: true };
|
|
630
517
|
}
|
|
631
|
-
|
|
632
518
|
reportViolations(violations);
|
|
633
519
|
return { success: false };
|
|
634
520
|
}
|
|
635
|
-
|
|
636
521
|
/**
|
|
637
522
|
* Resolve mode considering ignoreModifiedUntilEpoch override.
|
|
638
523
|
* When active, downgrades to OFF. When expired, logs a warning.
|
|
639
524
|
*/
|
|
640
|
-
function resolveMode(normalMode
|
|
525
|
+
function resolveMode(normalMode, epoch) {
|
|
641
526
|
if (epoch === undefined || normalMode === 'OFF') {
|
|
642
527
|
return normalMode;
|
|
643
528
|
}
|
|
@@ -650,48 +535,36 @@ function resolveMode(normalMode: ValidateDtosMode, epoch: number | undefined): V
|
|
|
650
535
|
}
|
|
651
536
|
return normalMode;
|
|
652
537
|
}
|
|
653
|
-
|
|
654
|
-
export default async function runValidator(
|
|
655
|
-
options: ValidateDtosOptions,
|
|
656
|
-
workspaceRoot: string
|
|
657
|
-
): Promise<ExecutorResult> {
|
|
538
|
+
async function runValidator(options, workspaceRoot) {
|
|
658
539
|
const mode = resolveMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch);
|
|
659
|
-
|
|
660
540
|
if (mode === 'OFF') {
|
|
661
541
|
console.log('\n⏭️ Skipping validate-dtos (mode: OFF)');
|
|
662
542
|
console.log('');
|
|
663
543
|
return { success: true };
|
|
664
544
|
}
|
|
665
|
-
|
|
666
545
|
const prismaSchemaPath = options.prismaSchemaPath;
|
|
667
546
|
const dtoSourcePaths = options.dtoSourcePaths ?? [];
|
|
668
|
-
|
|
669
547
|
if (!prismaSchemaPath || dtoSourcePaths.length === 0) {
|
|
670
548
|
const reason = !prismaSchemaPath ? 'no prismaSchemaPath configured' : 'no dtoSourcePaths configured';
|
|
671
549
|
console.log(`\n⏭️ Skipping validate-dtos (${reason})`);
|
|
672
550
|
console.log('');
|
|
673
551
|
return { success: true };
|
|
674
552
|
}
|
|
675
|
-
|
|
676
553
|
console.log('\n📏 Validating DTOs match Prisma Dbo models\n');
|
|
677
554
|
console.log(` Mode: ${mode}`);
|
|
678
555
|
console.log(` Schema: ${prismaSchemaPath}`);
|
|
679
556
|
console.log(` Dto paths: ${dtoSourcePaths.join(', ')}`);
|
|
680
|
-
|
|
681
557
|
const base = resolveBase(workspaceRoot);
|
|
682
558
|
const head = process.env['NX_HEAD'];
|
|
683
|
-
|
|
684
559
|
if (!base) {
|
|
685
560
|
console.log('\n⏭️ Skipping validate-dtos (could not detect base branch)');
|
|
686
561
|
console.log('');
|
|
687
562
|
return { success: true };
|
|
688
563
|
}
|
|
689
|
-
|
|
690
564
|
console.log(` Base: ${base}`);
|
|
691
565
|
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
692
566
|
console.log('');
|
|
693
|
-
|
|
694
567
|
const changedFiles = getChangedFiles(workspaceRoot, base, head);
|
|
695
|
-
|
|
696
568
|
return validateDtoFiles(workspaceRoot, prismaSchemaPath, changedFiles, dtoSourcePaths, mode, base, head);
|
|
697
569
|
}
|
|
570
|
+
//# sourceMappingURL=validate-dtos.js.map
|