@webpieces/code-rules 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/jest.config.ts +20 -0
- package/package.json +23 -0
- package/project.json +22 -0
- package/src/cli.ts +17 -0
- package/src/diff-utils.ts +129 -0
- package/src/from-shared-config.ts +118 -0
- package/src/index.ts +14 -0
- package/src/validate-catch-error-pattern.ts +639 -0
- package/src/validate-code.ts +491 -0
- package/src/validate-dtos.ts +697 -0
- package/src/validate-modified-files.ts +579 -0
- package/src/validate-modified-methods.ts +812 -0
- package/src/validate-new-methods.ts +594 -0
- package/src/validate-no-any-unknown.ts +552 -0
- package/src/validate-no-destructure.ts +588 -0
- package/src/validate-no-direct-api-resolver.ts +676 -0
- package/src/validate-no-implicit-any.ts +378 -0
- package/src/validate-no-inline-types.ts +787 -0
- package/src/validate-no-unmanaged-exceptions.ts +431 -0
- package/src/validate-prisma-converters.ts +830 -0
- package/src/validate-return-types.ts +532 -0
- package/tsconfig.json +22 -0
- package/tsconfig.lib.json +10 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate No Unmanaged Exceptions Executor
|
|
3
|
+
*
|
|
4
|
+
* Validates that try/catch blocks are not used outside chokepoints.
|
|
5
|
+
* Uses LINE-BASED detection (not method-based) for git diff filtering.
|
|
6
|
+
*
|
|
7
|
+
* ============================================================================
|
|
8
|
+
* VIOLATIONS (BAD) - These patterns are flagged:
|
|
9
|
+
* ============================================================================
|
|
10
|
+
*
|
|
11
|
+
* - try { — any try/catch block in non-test code
|
|
12
|
+
*
|
|
13
|
+
* ============================================================================
|
|
14
|
+
* ALLOWED (skip — NOT violations)
|
|
15
|
+
* ============================================================================
|
|
16
|
+
*
|
|
17
|
+
* - Test files (.spec.ts, .test.ts, __tests__/)
|
|
18
|
+
* - Lines with // webpieces-disable no-unmanaged-exceptions -- [reason]
|
|
19
|
+
*
|
|
20
|
+
* ============================================================================
|
|
21
|
+
* MODES (LINE-BASED)
|
|
22
|
+
* ============================================================================
|
|
23
|
+
* - OFF: Skip validation entirely
|
|
24
|
+
* - MODIFIED_CODE: Flag try/catch on changed lines (lines in diff hunks)
|
|
25
|
+
* - MODIFIED_FILES: Flag ALL try/catch in files that were modified
|
|
26
|
+
*
|
|
27
|
+
* ============================================================================
|
|
28
|
+
* ESCAPE HATCH
|
|
29
|
+
* ============================================================================
|
|
30
|
+
* Add comment above the violation:
|
|
31
|
+
* // webpieces-disable no-unmanaged-exceptions -- [your justification]
|
|
32
|
+
* try {
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { execSync } from 'child_process';
|
|
36
|
+
import * as fs from 'fs';
|
|
37
|
+
import * as path from 'path';
|
|
38
|
+
|
|
39
|
+
export type NoUnmanagedExceptionsMode = 'OFF' | 'MODIFIED_CODE' | 'MODIFIED_FILES';
|
|
40
|
+
|
|
41
|
+
export interface ValidateNoUnmanagedExceptionsOptions {
|
|
42
|
+
mode?: NoUnmanagedExceptionsMode;
|
|
43
|
+
disableAllowed?: boolean;
|
|
44
|
+
ignoreModifiedUntilEpoch?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ExecutorResult {
|
|
48
|
+
success: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface TryCatchViolation {
|
|
52
|
+
file: string;
|
|
53
|
+
line: number;
|
|
54
|
+
context: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a file is a test file that should be skipped.
|
|
59
|
+
*/
|
|
60
|
+
function isTestFile(filePath: string): boolean {
|
|
61
|
+
return filePath.includes('.spec.ts') ||
|
|
62
|
+
filePath.includes('.test.ts') ||
|
|
63
|
+
filePath.includes('__tests__/');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get changed TypeScript files between base and head (or working tree if head not specified).
|
|
68
|
+
* Excludes test files.
|
|
69
|
+
*/
|
|
70
|
+
// webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
|
|
71
|
+
function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: string): string[] {
|
|
72
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
73
|
+
try {
|
|
74
|
+
const diffTarget = head ? `${base} ${head}` : base;
|
|
75
|
+
const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
|
|
76
|
+
cwd: workspaceRoot,
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
});
|
|
79
|
+
const changedFiles = output
|
|
80
|
+
.trim()
|
|
81
|
+
.split('\n')
|
|
82
|
+
.filter((f) => f && !isTestFile(f));
|
|
83
|
+
|
|
84
|
+
if (!head) {
|
|
85
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
86
|
+
try {
|
|
87
|
+
const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
|
|
88
|
+
cwd: workspaceRoot,
|
|
89
|
+
encoding: 'utf-8',
|
|
90
|
+
});
|
|
91
|
+
const untrackedFiles = untrackedOutput
|
|
92
|
+
.trim()
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter((f) => f && !isTestFile(f));
|
|
95
|
+
const allFiles = new Set([...changedFiles, ...untrackedFiles]);
|
|
96
|
+
return Array.from(allFiles);
|
|
97
|
+
// webpieces-disable catch-error-pattern -- intentional swallow of git command failure
|
|
98
|
+
} catch {
|
|
99
|
+
return changedFiles;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return changedFiles;
|
|
104
|
+
// webpieces-disable catch-error-pattern -- intentional swallow of git command failure
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the diff content for a specific file.
|
|
112
|
+
*/
|
|
113
|
+
function getFileDiff(workspaceRoot: string, file: string, base: string, head?: string): string {
|
|
114
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
115
|
+
try {
|
|
116
|
+
const diffTarget = head ? `${base} ${head}` : base;
|
|
117
|
+
const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
|
|
118
|
+
cwd: workspaceRoot,
|
|
119
|
+
encoding: 'utf-8',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!diff && !head) {
|
|
123
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
124
|
+
if (fs.existsSync(fullPath)) {
|
|
125
|
+
const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
|
|
126
|
+
cwd: workspaceRoot,
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
}).trim();
|
|
129
|
+
|
|
130
|
+
if (isUntracked) {
|
|
131
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
return lines.map((line) => `+${line}`).join('\n');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return diff;
|
|
139
|
+
// webpieces-disable catch-error-pattern -- intentional swallow of git command failure
|
|
140
|
+
} catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse diff to extract changed line numbers (additions only - lines starting with +).
|
|
147
|
+
*/
|
|
148
|
+
function getChangedLineNumbers(diffContent: string): Set<number> {
|
|
149
|
+
const changedLines = new Set<number>();
|
|
150
|
+
const lines = diffContent.split('\n');
|
|
151
|
+
let currentLine = 0;
|
|
152
|
+
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
155
|
+
if (hunkMatch) {
|
|
156
|
+
currentLine = parseInt(hunkMatch[1], 10);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
161
|
+
changedLines.add(currentLine);
|
|
162
|
+
currentLine++;
|
|
163
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
164
|
+
// Deletions don't increment line number
|
|
165
|
+
} else {
|
|
166
|
+
currentLine++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return changedLines;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if a line contains a disable comment for no-unmanaged-exceptions.
|
|
175
|
+
* Recognizes both webpieces-disable and eslint-disable-next-line @webpieces/ formats.
|
|
176
|
+
*/
|
|
177
|
+
function hasDisableComment(lines: string[], lineNumber: number): boolean {
|
|
178
|
+
const startCheck = Math.max(0, lineNumber - 5);
|
|
179
|
+
for (let i = lineNumber - 2; i >= startCheck; i--) {
|
|
180
|
+
const line = lines[i]?.trim() ?? '';
|
|
181
|
+
if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (line.includes('webpieces-disable') && line.includes('no-unmanaged-exceptions')) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (line.includes('@webpieces/no-unmanaged-exceptions')) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const TRY_PATTERN = /\btry\s*\{/;
|
|
195
|
+
|
|
196
|
+
interface TryCatchInfo {
|
|
197
|
+
line: number;
|
|
198
|
+
context: string;
|
|
199
|
+
hasDisableComment: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find all try/catch patterns in a file using line-based scanning.
|
|
204
|
+
*/
|
|
205
|
+
function findTryCatchInFile(filePath: string, workspaceRoot: string, disableAllowed: boolean): TryCatchInfo[] {
|
|
206
|
+
const fullPath = path.join(workspaceRoot, filePath);
|
|
207
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
208
|
+
|
|
209
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
210
|
+
const fileLines = content.split('\n');
|
|
211
|
+
const violations: TryCatchInfo[] = [];
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
214
|
+
const line = fileLines[i];
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
// Skip comment lines (JSDoc, block comments, line comments)
|
|
217
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;
|
|
218
|
+
if (!TRY_PATTERN.test(line)) continue;
|
|
219
|
+
|
|
220
|
+
const lineNum = i + 1;
|
|
221
|
+
const disabled = hasDisableComment(fileLines, lineNum);
|
|
222
|
+
|
|
223
|
+
if (!disableAllowed && disabled) {
|
|
224
|
+
violations.push({ line: lineNum, context: line.trim(), hasDisableComment: false });
|
|
225
|
+
} else {
|
|
226
|
+
violations.push({ line: lineNum, context: line.trim(), hasDisableComment: disabled });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return violations;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* MODIFIED_CODE mode: Flag violations on changed lines in diff hunks.
|
|
235
|
+
*/
|
|
236
|
+
function findViolationsForModifiedCode(
|
|
237
|
+
workspaceRoot: string,
|
|
238
|
+
changedFiles: string[],
|
|
239
|
+
base: string,
|
|
240
|
+
head: string | undefined,
|
|
241
|
+
disableAllowed: boolean,
|
|
242
|
+
): TryCatchViolation[] {
|
|
243
|
+
const violations: TryCatchViolation[] = [];
|
|
244
|
+
|
|
245
|
+
for (const file of changedFiles) {
|
|
246
|
+
const diff = getFileDiff(workspaceRoot, file, base, head);
|
|
247
|
+
const changedLines = getChangedLineNumbers(diff);
|
|
248
|
+
|
|
249
|
+
if (changedLines.size === 0) continue;
|
|
250
|
+
|
|
251
|
+
const allViolations = findTryCatchInFile(file, workspaceRoot, disableAllowed);
|
|
252
|
+
|
|
253
|
+
for (const v of allViolations) {
|
|
254
|
+
if (disableAllowed && v.hasDisableComment) continue;
|
|
255
|
+
if (!changedLines.has(v.line)) continue;
|
|
256
|
+
|
|
257
|
+
violations.push({ file, line: v.line, context: v.context });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return violations;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* MODIFIED_FILES mode: Flag ALL violations in files that were modified.
|
|
266
|
+
*/
|
|
267
|
+
function findViolationsForModifiedFiles(
|
|
268
|
+
workspaceRoot: string,
|
|
269
|
+
changedFiles: string[],
|
|
270
|
+
disableAllowed: boolean,
|
|
271
|
+
): TryCatchViolation[] {
|
|
272
|
+
const violations: TryCatchViolation[] = [];
|
|
273
|
+
|
|
274
|
+
for (const file of changedFiles) {
|
|
275
|
+
const allViolations = findTryCatchInFile(file, workspaceRoot, disableAllowed);
|
|
276
|
+
|
|
277
|
+
for (const v of allViolations) {
|
|
278
|
+
if (disableAllowed && v.hasDisableComment) continue;
|
|
279
|
+
violations.push({ file, line: v.line, context: v.context });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return violations;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Auto-detect the base branch by finding the merge-base with origin/main.
|
|
288
|
+
*/
|
|
289
|
+
function detectBase(workspaceRoot: string): string | null {
|
|
290
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
291
|
+
try {
|
|
292
|
+
const mergeBase = execSync('git merge-base HEAD origin/main', {
|
|
293
|
+
cwd: workspaceRoot,
|
|
294
|
+
encoding: 'utf-8',
|
|
295
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
296
|
+
}).trim();
|
|
297
|
+
|
|
298
|
+
if (mergeBase) {
|
|
299
|
+
return mergeBase;
|
|
300
|
+
}
|
|
301
|
+
// webpieces-disable catch-error-pattern -- intentional swallow of git command failure
|
|
302
|
+
} catch {
|
|
303
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
304
|
+
try {
|
|
305
|
+
const mergeBase = execSync('git merge-base HEAD main', {
|
|
306
|
+
cwd: workspaceRoot,
|
|
307
|
+
encoding: 'utf-8',
|
|
308
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
309
|
+
}).trim();
|
|
310
|
+
|
|
311
|
+
if (mergeBase) {
|
|
312
|
+
return mergeBase;
|
|
313
|
+
}
|
|
314
|
+
// webpieces-disable catch-error-pattern -- intentional swallow of git command failure
|
|
315
|
+
} catch {
|
|
316
|
+
// Ignore
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Report violations to console.
|
|
324
|
+
*/
|
|
325
|
+
function reportViolations(violations: TryCatchViolation[], mode: NoUnmanagedExceptionsMode, disableAllowed: boolean): void {
|
|
326
|
+
console.error('');
|
|
327
|
+
console.error('\u274c Unmanaged try/catch blocks found! Exceptions should bubble to chokepoints.');
|
|
328
|
+
console.error('');
|
|
329
|
+
console.error('\ud83d\udcda Philosophy: Most code should NOT catch exceptions.');
|
|
330
|
+
console.error(' Exceptions should bubble to chokepoints (filter in Node.js, globalErrorHandler in Angular)');
|
|
331
|
+
console.error(' where they are logged with traceId for debugging.');
|
|
332
|
+
console.error('');
|
|
333
|
+
|
|
334
|
+
for (const v of violations) {
|
|
335
|
+
console.error(` \u274c ${v.file}:${v.line}`);
|
|
336
|
+
console.error(` ${v.context}`);
|
|
337
|
+
}
|
|
338
|
+
console.error('');
|
|
339
|
+
|
|
340
|
+
if (disableAllowed) {
|
|
341
|
+
console.error(' Escape hatch (use sparingly):');
|
|
342
|
+
console.error(' // webpieces-disable no-unmanaged-exceptions -- [your reason]');
|
|
343
|
+
console.error('');
|
|
344
|
+
console.error(' When try/catch IS used, the catch block MUST follow:');
|
|
345
|
+
console.error(' catch (err: unknown) { const error = toError(err); ... }');
|
|
346
|
+
console.error(' or: catch (err: unknown) { //const error = toError(err); }');
|
|
347
|
+
} else {
|
|
348
|
+
console.error(' Escape hatch: DISABLED (disableAllowed: false)');
|
|
349
|
+
console.error(' Disable comments are ignored. Remove the try/catch.');
|
|
350
|
+
}
|
|
351
|
+
console.error('');
|
|
352
|
+
console.error(` Current mode: ${mode}`);
|
|
353
|
+
console.error('');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Resolve mode considering ignoreModifiedUntilEpoch override.
|
|
358
|
+
*/
|
|
359
|
+
function resolveMode(normalMode: NoUnmanagedExceptionsMode, epoch: number | undefined): NoUnmanagedExceptionsMode {
|
|
360
|
+
if (epoch === undefined || normalMode === 'OFF') {
|
|
361
|
+
return normalMode;
|
|
362
|
+
}
|
|
363
|
+
const nowSeconds = Date.now() / 1000;
|
|
364
|
+
if (nowSeconds < epoch) {
|
|
365
|
+
const expiresDate = new Date(epoch * 1000).toISOString().split('T')[0];
|
|
366
|
+
console.log(`\n\u23ed\ufe0f Skipping no-unmanaged-exceptions validation (ignoreModifiedUntilEpoch active, expires: ${expiresDate})`);
|
|
367
|
+
console.log('');
|
|
368
|
+
return 'OFF';
|
|
369
|
+
}
|
|
370
|
+
return normalMode;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export default async function runValidator(
|
|
374
|
+
options: ValidateNoUnmanagedExceptionsOptions,
|
|
375
|
+
workspaceRoot: string
|
|
376
|
+
): Promise<ExecutorResult> {
|
|
377
|
+
const mode: NoUnmanagedExceptionsMode = resolveMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch);
|
|
378
|
+
const disableAllowed = options.disableAllowed ?? true;
|
|
379
|
+
|
|
380
|
+
if (mode === 'OFF') {
|
|
381
|
+
console.log('\n\u23ed\ufe0f Skipping no-unmanaged-exceptions validation (mode: OFF)');
|
|
382
|
+
console.log('');
|
|
383
|
+
return { success: true };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
console.log('\n\ud83d\udccf Validating No Unmanaged Exceptions\n');
|
|
387
|
+
console.log(` Mode: ${mode}`);
|
|
388
|
+
|
|
389
|
+
let base = process.env['NX_BASE'];
|
|
390
|
+
const head = process.env['NX_HEAD'];
|
|
391
|
+
|
|
392
|
+
if (!base) {
|
|
393
|
+
base = detectBase(workspaceRoot) ?? undefined;
|
|
394
|
+
|
|
395
|
+
if (!base) {
|
|
396
|
+
console.log('\n\u23ed\ufe0f Skipping no-unmanaged-exceptions validation (could not detect base branch)');
|
|
397
|
+
console.log('');
|
|
398
|
+
return { success: true };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(` Base: ${base}`);
|
|
403
|
+
console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
|
|
404
|
+
console.log('');
|
|
405
|
+
|
|
406
|
+
const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
|
|
407
|
+
|
|
408
|
+
if (changedFiles.length === 0) {
|
|
409
|
+
console.log('\u2705 No TypeScript files changed');
|
|
410
|
+
return { success: true };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(`\ud83d\udcc2 Checking ${changedFiles.length} changed file(s)...`);
|
|
414
|
+
|
|
415
|
+
let violations: TryCatchViolation[] = [];
|
|
416
|
+
|
|
417
|
+
if (mode === 'MODIFIED_CODE') {
|
|
418
|
+
violations = findViolationsForModifiedCode(workspaceRoot, changedFiles, base, head, disableAllowed);
|
|
419
|
+
} else if (mode === 'MODIFIED_FILES') {
|
|
420
|
+
violations = findViolationsForModifiedFiles(workspaceRoot, changedFiles, disableAllowed);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (violations.length === 0) {
|
|
424
|
+
console.log('\u2705 No unmanaged try/catch blocks found');
|
|
425
|
+
return { success: true };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
reportViolations(violations, mode, disableAllowed);
|
|
429
|
+
|
|
430
|
+
return { success: false };
|
|
431
|
+
}
|