yini-cli 1.3.4 → 1.4.0
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/README.md +1 -1
- package/dist/commands/commonFunctions.d.ts +15 -0
- package/dist/commands/commonFunctions.js +76 -0
- package/dist/commands/parseCommand.d.ts +1 -1
- package/dist/commands/parseCommand.js +64 -51
- package/dist/commands/validateCommand.d.ts +6 -1
- package/dist/commands/validateCommand.js +440 -402
- package/dist/descriptions.js +6 -1
- package/dist/globalOptions/helpOption.js +9 -6
- package/dist/index.js +51 -31
- package/dist/utils/string.js +1 -0
- package/package.json +2 -2
|
@@ -1,446 +1,484 @@
|
|
|
1
|
-
import { exit } from 'node:process';
|
|
2
|
-
import YINI from 'yini-parser';
|
|
3
|
-
const IS_DEBUG = false; // For local debugging purposes, etc.
|
|
4
1
|
/*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
File: config.yini
|
|
60
|
-
Mode: lenient
|
|
61
|
-
Errors: 0
|
|
62
|
-
Warnings: 2
|
|
63
|
-
|
|
64
|
-
On failure:
|
|
65
|
-
✖ Validation failed
|
|
66
|
-
File: config.yini
|
|
67
|
-
Mode: strict
|
|
68
|
-
Errors: 3
|
|
69
|
-
Warnings: 1
|
|
70
|
-
|
|
71
|
-
* Issues sections:
|
|
72
|
-
Each issue:
|
|
73
|
-
Severity error / warning
|
|
74
|
-
Code stable identifier (DUPLICATE_KEY, UNKNOWN_CONSTRUCT, etc.)
|
|
75
|
-
Message short explanation
|
|
76
|
-
Location file + line + column
|
|
77
|
-
Context snippet of the file (if helpful)
|
|
78
|
-
|
|
79
|
-
Example:
|
|
80
|
-
Errors:
|
|
81
|
-
[E001] Duplicate key "host"
|
|
82
|
-
at config.yini:14:5
|
|
83
|
-
Previous definition at line 7
|
|
84
|
-
→ host = "localhost"
|
|
85
|
-
|
|
86
|
-
Warnings:
|
|
87
|
-
[W002] Reserved construct used: "$schema"
|
|
88
|
-
at config.yini:3:1
|
|
89
|
-
|
|
90
|
-
* Optional meta-data section --stats
|
|
91
|
-
Statistics:
|
|
92
|
-
Sections: 5
|
|
93
|
-
Keys: 27
|
|
94
|
-
Arrays: 4
|
|
95
|
-
Objects: 3
|
|
96
|
-
Nesting depth: 4
|
|
97
|
-
|
|
98
|
-
* Exit code contract
|
|
99
|
-
Success, no warnings 0
|
|
100
|
-
Warnings only 0 (or 1 if --warnings-as-errors)
|
|
101
|
-
Errors 1
|
|
102
|
-
|
|
103
|
-
Example: JSON format (--format json)
|
|
104
|
-
{
|
|
105
|
-
"file": "config.yini",
|
|
106
|
-
"mode": "strict",
|
|
107
|
-
"summary": {
|
|
108
|
-
"errors": 2,
|
|
109
|
-
"warnings": 1
|
|
110
|
-
},
|
|
111
|
-
"issues": [
|
|
112
|
-
{
|
|
113
|
-
"severity": "error",
|
|
114
|
-
"code": "DUPLICATE_KEY",
|
|
115
|
-
"message": "Duplicate key \"host\"",
|
|
116
|
-
"location": { "line": 14, "column": 5 }
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
"severity": "error",
|
|
120
|
-
"code": "INVALID_TYPE",
|
|
121
|
-
"message": "Expected number, got string",
|
|
122
|
-
"location": { "line": 22, "column": 12 }
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"severity": "warning",
|
|
126
|
-
"code": "RESERVED_CONSTRUCT",
|
|
127
|
-
"message": "Reserved construct \"$schema\"",
|
|
128
|
-
"location": { "line": 3, "column": 1 }
|
|
2
|
+
* validateCommand.ts
|
|
3
|
+
*
|
|
4
|
+
* Behavioral spec and output conventions:
|
|
5
|
+
* see docs/cli/validate-command.md
|
|
6
|
+
*/
|
|
7
|
+
// src/commands/validateCommand.ts
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import YINI from 'yini-parser';
|
|
11
|
+
import { getDisplayBaseDir, printStderr, printStdout, resolveRunModeFromTargets, resolveStrictMode, toDisplayPath, } from './commonFunctions.js';
|
|
12
|
+
// --- Public entrypoint -------------------------------------------------------
|
|
13
|
+
export const validateTargets = (targets, options) => {
|
|
14
|
+
try {
|
|
15
|
+
const strictMode = resolveStrictMode(options);
|
|
16
|
+
const mode = strictMode ? 'strict' : 'lenient';
|
|
17
|
+
const runMode = resolveRunModeFromTargets(targets);
|
|
18
|
+
const recursive = options.recursive ?? true;
|
|
19
|
+
const files = collectFilesFromTargets(targets, recursive);
|
|
20
|
+
if (!files.length) {
|
|
21
|
+
throw new Error('No YINI files found to validate.');
|
|
22
|
+
}
|
|
23
|
+
const displayBaseDir = getDisplayBaseDir(targets);
|
|
24
|
+
const aggregate = {
|
|
25
|
+
runMode,
|
|
26
|
+
mode,
|
|
27
|
+
status: 'passed',
|
|
28
|
+
filesChecked: 0,
|
|
29
|
+
failedFiles: 0,
|
|
30
|
+
errors: 0,
|
|
31
|
+
warnings: 0,
|
|
32
|
+
notices: 0,
|
|
33
|
+
infos: 0,
|
|
34
|
+
displayBaseDir,
|
|
35
|
+
results: [],
|
|
36
|
+
};
|
|
37
|
+
let totalErrorsSeen = 0;
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const result = validateOneFile(file, options);
|
|
40
|
+
aggregate.results.push(result);
|
|
41
|
+
aggregate.filesChecked += 1;
|
|
42
|
+
aggregate.errors += result.errors;
|
|
43
|
+
aggregate.warnings += result.warnings;
|
|
44
|
+
aggregate.notices += result.notices;
|
|
45
|
+
aggregate.infos += result.infos;
|
|
46
|
+
if (result.status === 'failed') {
|
|
47
|
+
aggregate.failedFiles += 1;
|
|
48
|
+
}
|
|
49
|
+
totalErrorsSeen += result.errors;
|
|
50
|
+
if (options.failFast && result.status === 'failed') {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (typeof options.maxErrors === 'number' &&
|
|
54
|
+
totalErrorsSeen >= options.maxErrors) {
|
|
55
|
+
break;
|
|
129
56
|
}
|
|
130
|
-
],
|
|
131
|
-
"stats": {
|
|
132
|
-
"sections": 5,
|
|
133
|
-
"keys": 27,
|
|
134
|
-
"nestingDepth": 4
|
|
135
57
|
}
|
|
58
|
+
aggregate.status = resolveAggregateStatus(aggregate, options);
|
|
59
|
+
if (!options.silent) {
|
|
60
|
+
if (options.format === 'json') {
|
|
61
|
+
printStdout(options, formatJsonReport(aggregate, options));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
printTextReport(aggregate, options);
|
|
65
|
+
}
|
|
136
66
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
67
|
+
process.exit(getValidationExitCode(aggregate, options));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
printStderr(options, `Error: ${message}`);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
// --- File collection ---------------------------------------------------------
|
|
76
|
+
const collectFilesFromTargets = (targets, recursive) => {
|
|
77
|
+
const out = new Set();
|
|
78
|
+
for (const target of targets) {
|
|
79
|
+
const resolved = path.resolve(target);
|
|
80
|
+
if (!fs.existsSync(resolved)) {
|
|
81
|
+
throw new Error(`Path does not exist: "${target}"`);
|
|
82
|
+
}
|
|
83
|
+
const stat = fs.statSync(resolved);
|
|
84
|
+
if (stat.isFile()) {
|
|
85
|
+
out.add(resolved);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!stat.isDirectory()) {
|
|
89
|
+
throw new Error(`Not a file or directory: "${target}"`);
|
|
90
|
+
}
|
|
91
|
+
for (const file of collectFilesFromDirectory(resolved, recursive)) {
|
|
92
|
+
out.add(file);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...out].sort((a, b) => a.localeCompare(b));
|
|
96
|
+
};
|
|
97
|
+
const collectFilesFromDirectory = (dirPath, recursive) => {
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const entry of fs.readdirSync(dirPath)) {
|
|
100
|
+
const fullPath = path.join(dirPath, entry);
|
|
101
|
+
const stat = fs.statSync(fullPath);
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
if (recursive) {
|
|
104
|
+
results.push(...collectFilesFromDirectory(fullPath, recursive));
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (stat.isFile() && fullPath.toLowerCase().endsWith('.yini')) {
|
|
109
|
+
results.push(fullPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
};
|
|
114
|
+
// --- Validation --------------------------------------------------------------
|
|
115
|
+
const validateOneFile = (file, options) => {
|
|
116
|
+
const strictMode = resolveStrictMode(options);
|
|
142
117
|
const parseOptions = {
|
|
143
|
-
strictMode
|
|
118
|
+
strictMode,
|
|
144
119
|
failLevel: 'ignore-errors',
|
|
145
120
|
includeMetadata: true,
|
|
146
121
|
includeDiagnostics: true,
|
|
147
122
|
silent: true,
|
|
148
123
|
};
|
|
149
124
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
125
|
+
const parsed = YINI.parseFile(file, parseOptions);
|
|
126
|
+
const metadata = parsed?.meta ?? null;
|
|
127
|
+
const diagnostics = metadata?.diagnostics;
|
|
128
|
+
if (!diagnostics) {
|
|
129
|
+
return {
|
|
130
|
+
file,
|
|
131
|
+
mode: strictMode ? 'strict' : 'lenient',
|
|
132
|
+
status: 'failed',
|
|
133
|
+
errors: 1,
|
|
134
|
+
warnings: 0,
|
|
135
|
+
notices: 0,
|
|
136
|
+
infos: 0,
|
|
137
|
+
issues: [
|
|
138
|
+
{
|
|
139
|
+
severity: 'error',
|
|
140
|
+
code: 'MISSING_DIAGNOSTICS',
|
|
141
|
+
message: 'Missing diagnostics metadata.',
|
|
142
|
+
line: 0,
|
|
143
|
+
column: 0,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
metadata,
|
|
147
|
+
fatalError: 'Missing diagnostics metadata.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const issues = [
|
|
151
|
+
...mapIssuePayloadArray('error', diagnostics.errors.payload),
|
|
152
|
+
...mapIssuePayloadArray('warning', diagnostics.warnings.payload),
|
|
153
|
+
...mapIssuePayloadArray('notice', diagnostics.notices.payload),
|
|
154
|
+
...mapIssuePayloadArray('info', diagnostics.infos.payload),
|
|
155
|
+
];
|
|
156
|
+
const errors = diagnostics.errors.errorCount;
|
|
157
|
+
const warnings = diagnostics.warnings.warningCount;
|
|
158
|
+
const notices = diagnostics.notices.noticeCount;
|
|
159
|
+
const infos = diagnostics.infos.infoCount;
|
|
160
|
+
return {
|
|
161
|
+
file,
|
|
162
|
+
mode: metadata?.mode === 'strict' || metadata?.mode === 'lenient'
|
|
163
|
+
? metadata.mode
|
|
164
|
+
: 'custom',
|
|
165
|
+
status: resolveFileStatus(errors, warnings, options),
|
|
166
|
+
errors,
|
|
167
|
+
warnings,
|
|
168
|
+
notices,
|
|
169
|
+
infos,
|
|
170
|
+
issues,
|
|
171
|
+
metadata,
|
|
172
|
+
};
|
|
152
173
|
}
|
|
153
174
|
catch (err) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
176
|
+
return {
|
|
177
|
+
file,
|
|
178
|
+
mode: strictMode ? 'strict' : 'lenient',
|
|
179
|
+
status: 'failed',
|
|
180
|
+
errors: 1,
|
|
181
|
+
warnings: 0,
|
|
182
|
+
notices: 0,
|
|
183
|
+
infos: 0,
|
|
184
|
+
issues: [
|
|
185
|
+
{
|
|
186
|
+
severity: 'error',
|
|
187
|
+
code: 'RUNTIME_ERROR',
|
|
188
|
+
message,
|
|
189
|
+
line: 0,
|
|
190
|
+
column: 0,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
metadata: null,
|
|
194
|
+
fatalError: message,
|
|
195
|
+
};
|
|
159
196
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
};
|
|
198
|
+
const mapIssuePayloadArray = (severity, payload) => {
|
|
199
|
+
return payload.map((item) => ({
|
|
200
|
+
severity,
|
|
201
|
+
code: toStableIssueCode(item.typeKey, severity),
|
|
202
|
+
message: item.message ?? 'Unknown issue.',
|
|
203
|
+
line: item.line ?? 0,
|
|
204
|
+
column: item.column ?? 0,
|
|
205
|
+
advice: item.advice,
|
|
206
|
+
hint: item.hint,
|
|
207
|
+
}));
|
|
208
|
+
};
|
|
209
|
+
const toStableIssueCode = (typeKey, fallbackSeverity) => {
|
|
210
|
+
const base = typeKey && typeKey.trim() ? typeKey : fallbackSeverity;
|
|
211
|
+
return base
|
|
212
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
213
|
+
.replace(/^_+|_+$/g, '')
|
|
214
|
+
.toUpperCase();
|
|
215
|
+
};
|
|
216
|
+
// --- Status / exit code resolution ------------------------------------------
|
|
217
|
+
const resolveFileStatus = (errors, warnings, options) => {
|
|
218
|
+
if (errors > 0)
|
|
219
|
+
return 'failed';
|
|
220
|
+
if (options.warningsAsErrors && warnings > 0)
|
|
221
|
+
return 'failed';
|
|
222
|
+
if (warnings > 0)
|
|
223
|
+
return 'passed-with-warnings';
|
|
224
|
+
return 'passed';
|
|
225
|
+
};
|
|
226
|
+
const resolveAggregateStatus = (aggregate, options) => {
|
|
227
|
+
if (aggregate.failedFiles > 0)
|
|
228
|
+
return 'failed';
|
|
229
|
+
if (options.warningsAsErrors && aggregate.warnings > 0)
|
|
230
|
+
return 'failed';
|
|
231
|
+
if (aggregate.warnings > 0)
|
|
232
|
+
return 'passed-with-warnings';
|
|
233
|
+
return 'passed';
|
|
234
|
+
};
|
|
235
|
+
const getValidationExitCode = (aggregate, options) => {
|
|
236
|
+
if (aggregate.failedFiles > 0)
|
|
237
|
+
return 1;
|
|
238
|
+
if (options.warningsAsErrors && aggregate.warnings > 0)
|
|
239
|
+
return 1;
|
|
240
|
+
return 0;
|
|
241
|
+
};
|
|
242
|
+
// --- Text output -------------------------------------------------------------
|
|
243
|
+
const printTextReport = (aggregate, options) => {
|
|
244
|
+
if (aggregate.runMode === 'directory') {
|
|
245
|
+
printDirectoryModeTextReport(aggregate, options);
|
|
246
|
+
return;
|
|
179
247
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
IS_DEBUG && console.log(' errors = ' + errors);
|
|
184
|
-
IS_DEBUG && console.log('warnings = ' + warnings);
|
|
185
|
-
IS_DEBUG && console.log(' notices = ' + notices);
|
|
186
|
-
IS_DEBUG && console.log(' infos = ' + infos);
|
|
187
|
-
IS_DEBUG && console.log('metadata = ' + metadata);
|
|
188
|
-
IS_DEBUG &&
|
|
189
|
-
console.log('includeMetadata = ' +
|
|
190
|
-
metadata?.diagnostics?.effectiveOptions.includeMetadata);
|
|
191
|
-
IS_DEBUG && console.log('commandOptions.stats = ' + commandOptions?.stats);
|
|
192
|
-
IS_DEBUG && console.log();
|
|
193
|
-
//state returned:
|
|
194
|
-
// - passed (no errors/warnings),
|
|
195
|
-
// - finished (with warnings, no errors) / or - passed with warnings
|
|
196
|
-
// - failed (errors),
|
|
197
|
-
if (isCatchedError) {
|
|
198
|
-
errors = 1;
|
|
248
|
+
if (aggregate.results.length === 1) {
|
|
249
|
+
printSingleFileTextReport(aggregate.results[0], options);
|
|
250
|
+
return;
|
|
199
251
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
252
|
+
printFileModeTextReport(aggregate, options);
|
|
253
|
+
};
|
|
254
|
+
const printSingleFileTextReport = (result, options) => {
|
|
255
|
+
const totalIssues = result.errors + result.warnings + result.notices + result.infos;
|
|
256
|
+
if (result.status === 'failed') {
|
|
257
|
+
const suffix = totalIssues > 0 ? ` (${totalIssues} issues)` : '';
|
|
258
|
+
printStdout(options, `✖ Validation failed${suffix}`);
|
|
204
259
|
}
|
|
205
|
-
else if (warnings) {
|
|
206
|
-
|
|
260
|
+
else if (result.status === 'passed-with-warnings') {
|
|
261
|
+
printStdout(options, '✔ Validation successful (with warnings)');
|
|
207
262
|
}
|
|
208
263
|
else {
|
|
209
|
-
|
|
264
|
+
printStdout(options, '✔ Validation successful');
|
|
210
265
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// errors,
|
|
227
|
-
// warnings,
|
|
228
|
-
// notices,
|
|
229
|
-
// infos,
|
|
230
|
-
// ),
|
|
231
|
-
// )
|
|
232
|
-
// // exit(0)
|
|
233
|
-
// } else {
|
|
234
|
-
// // green ✔
|
|
235
|
-
// console.log(formatToSummary('Passed', errors, warnings, notices, infos))
|
|
236
|
-
// // exit(0)
|
|
237
|
-
// }
|
|
238
|
-
// Print optional Stats-report if "--stats" was given.
|
|
239
|
-
if (!commandOptions.silent && !isCatchedError) {
|
|
240
|
-
// if (commandOptions.details) {
|
|
241
|
-
if (errors || warnings) {
|
|
242
|
-
if (!metadata) {
|
|
243
|
-
console.error('Internal error: No metadata available');
|
|
244
|
-
exit(1);
|
|
245
|
-
}
|
|
246
|
-
// assert(metadata) // Make sure there is metadata!
|
|
247
|
-
console.log();
|
|
248
|
-
printIssuesFound(file, metadata);
|
|
266
|
+
printStdout(options, '');
|
|
267
|
+
printStdout(options, `File: "${result.file}"`);
|
|
268
|
+
printStdout(options, `Mode: ${result.mode}`);
|
|
269
|
+
printStdout(options, `Errors: ${result.errors}`);
|
|
270
|
+
printStdout(options, `Warnings: ${result.warnings}`);
|
|
271
|
+
printIssueDetailsForResult(result, options);
|
|
272
|
+
if (options.stats && result.metadata) {
|
|
273
|
+
printStdout(options, '');
|
|
274
|
+
printStdout(options, formatStatsReport(result.metadata));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const printFileModeTextReport = (aggregate, options) => {
|
|
278
|
+
for (const result of aggregate.results) {
|
|
279
|
+
if (result.status === 'failed') {
|
|
280
|
+
printStdout(options, `FAIL "${result.file}"`);
|
|
249
281
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
console.error('Internal error: No metadata available');
|
|
253
|
-
exit(1);
|
|
254
|
-
}
|
|
255
|
-
console.log();
|
|
256
|
-
console.log(formatToStatsReport(file, metadata).trim());
|
|
282
|
+
else if (!options.quiet) {
|
|
283
|
+
printStdout(options, `OK "${result.file}"`);
|
|
257
284
|
}
|
|
258
285
|
}
|
|
259
|
-
|
|
260
|
-
|
|
286
|
+
printStdout(options, '');
|
|
287
|
+
printStdout(options, `Mode: ${aggregate.mode}`);
|
|
288
|
+
printStdout(options, `Summary: ${aggregate.filesChecked} checked, ${aggregate.failedFiles} failed, ${aggregate.errors} errors, ${aggregate.warnings} warnings`);
|
|
289
|
+
for (const result of aggregate.results) {
|
|
290
|
+
if (result.status !== 'failed')
|
|
291
|
+
continue;
|
|
292
|
+
printIssueDetailsForResult(result, options);
|
|
261
293
|
}
|
|
262
|
-
exit(0);
|
|
263
294
|
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
const printDirectoryModeTextReport = (aggregate, options) => {
|
|
296
|
+
for (const result of aggregate.results) {
|
|
297
|
+
const displayPath = toDisplayPath(result.file, aggregate.displayBaseDir);
|
|
298
|
+
if (result.status === 'failed') {
|
|
299
|
+
printStdout(options, `FAIL "${displayPath}"`);
|
|
300
|
+
}
|
|
301
|
+
else if (!options.quiet) {
|
|
302
|
+
printStdout(options, `OK "${displayPath}"`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
printStdout(options, '');
|
|
306
|
+
printStdout(options, `Base: "${aggregate.displayBaseDir}"`);
|
|
307
|
+
printStdout(options, `Mode: ${aggregate.mode}`);
|
|
308
|
+
printStdout(options, `Summary: ${aggregate.filesChecked} checked, ${aggregate.failedFiles} failed, ${aggregate.errors} errors, ${aggregate.warnings} warnings`);
|
|
309
|
+
for (const result of aggregate.results) {
|
|
310
|
+
if (result.status !== 'failed')
|
|
311
|
+
continue;
|
|
312
|
+
printIssueDetailsForResult(result, options, aggregate.displayBaseDir);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const printIssueDetailsForResult = (result, options, displayBaseDir) => {
|
|
316
|
+
const printable = result.issues.filter((issue) => issue.severity === 'error' || issue.severity === 'warning');
|
|
317
|
+
if (!printable.length)
|
|
318
|
+
return;
|
|
319
|
+
const displayPath = displayBaseDir
|
|
320
|
+
? toDisplayPath(result.file, displayBaseDir)
|
|
321
|
+
: result.file;
|
|
322
|
+
printStderr(options, '');
|
|
323
|
+
printStderr(options, `"${displayPath}"`);
|
|
324
|
+
printable.forEach((issue, index) => {
|
|
325
|
+
const hasLocation = typeof issue.line === 'number' &&
|
|
326
|
+
typeof issue.column === 'number' &&
|
|
327
|
+
issue.line > 0 &&
|
|
328
|
+
issue.column > 0;
|
|
329
|
+
const location = hasLocation
|
|
330
|
+
? `${issue.line}:${issue.column}`.padEnd(7)
|
|
331
|
+
: ''.padEnd(7);
|
|
332
|
+
const severity = issue.severity.padEnd(8);
|
|
333
|
+
printStderr(options, hasLocation
|
|
334
|
+
? ` ${location} ${severity}${issue.message}`
|
|
335
|
+
: ` ${severity}${issue.message}`);
|
|
336
|
+
if (issue.advice && !options.quiet) {
|
|
337
|
+
printStderr(options, ` ${issue.advice}`);
|
|
338
|
+
}
|
|
339
|
+
if (issue.hint && !options.quiet) {
|
|
340
|
+
printStderr(options, ` ${issue.hint}`);
|
|
341
|
+
}
|
|
342
|
+
if (index < printable.length - 1) {
|
|
343
|
+
printStderr(options, '');
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
// --- JSON output -------------------------------------------------------------
|
|
348
|
+
const formatJsonReport = (aggregate, options) => {
|
|
349
|
+
if (aggregate.runMode === 'directory') {
|
|
350
|
+
return JSON.stringify(toDirectoryModeJsonReport(aggregate, options), null, 2);
|
|
351
|
+
}
|
|
352
|
+
if (aggregate.results.length === 1) {
|
|
353
|
+
return JSON.stringify(toSingleFileJsonReport(aggregate.results[0], options), null, 2);
|
|
284
354
|
}
|
|
285
|
-
|
|
286
|
-
|
|
355
|
+
return JSON.stringify(toFileModeJsonReport(aggregate, options), null, 2);
|
|
356
|
+
};
|
|
357
|
+
const toSingleFileJsonReport = (result, options) => {
|
|
287
358
|
return {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
mode: mode,
|
|
359
|
+
file: result.file,
|
|
360
|
+
runMode: 'file',
|
|
361
|
+
mode: result.mode,
|
|
362
|
+
status: result.status,
|
|
291
363
|
summary: {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
infos,
|
|
364
|
+
errors: result.errors,
|
|
365
|
+
warnings: result.warnings,
|
|
366
|
+
notices: result.notices,
|
|
367
|
+
infos: result.infos,
|
|
297
368
|
},
|
|
369
|
+
issues: result.issues.map(mapIssueToJson),
|
|
370
|
+
stats: toStatsJson(result.metadata, options),
|
|
298
371
|
};
|
|
299
372
|
};
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
373
|
+
const toFileModeJsonReport = (aggregate, options) => {
|
|
374
|
+
return {
|
|
375
|
+
runMode: 'file',
|
|
376
|
+
mode: aggregate.mode,
|
|
377
|
+
status: aggregate.status,
|
|
378
|
+
summary: {
|
|
379
|
+
filesChecked: aggregate.filesChecked,
|
|
380
|
+
failedFiles: aggregate.failedFiles,
|
|
381
|
+
errors: aggregate.errors,
|
|
382
|
+
warnings: aggregate.warnings,
|
|
383
|
+
notices: aggregate.notices,
|
|
384
|
+
infos: aggregate.infos,
|
|
385
|
+
},
|
|
386
|
+
files: aggregate.results.map((result) => ({
|
|
387
|
+
file: result.file,
|
|
388
|
+
mode: result.mode,
|
|
389
|
+
status: result.status,
|
|
390
|
+
summary: {
|
|
391
|
+
errors: result.errors,
|
|
392
|
+
warnings: result.warnings,
|
|
393
|
+
notices: result.notices,
|
|
394
|
+
infos: result.infos,
|
|
395
|
+
},
|
|
396
|
+
issues: result.issues.map(mapIssueToJson),
|
|
397
|
+
stats: toStatsJson(result.metadata, options),
|
|
398
|
+
})),
|
|
399
|
+
};
|
|
316
400
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
401
|
+
const toDirectoryModeJsonReport = (aggregate, options) => {
|
|
402
|
+
return {
|
|
403
|
+
base: aggregate.displayBaseDir,
|
|
404
|
+
runMode: 'directory',
|
|
405
|
+
mode: aggregate.mode,
|
|
406
|
+
status: aggregate.status,
|
|
407
|
+
summary: {
|
|
408
|
+
filesChecked: aggregate.filesChecked,
|
|
409
|
+
failedFiles: aggregate.failedFiles,
|
|
410
|
+
errors: aggregate.errors,
|
|
411
|
+
warnings: aggregate.warnings,
|
|
412
|
+
notices: aggregate.notices,
|
|
413
|
+
infos: aggregate.infos,
|
|
414
|
+
},
|
|
415
|
+
files: aggregate.results.map((result) => ({
|
|
416
|
+
file: toDisplayPath(result.file, aggregate.displayBaseDir),
|
|
417
|
+
mode: result.mode,
|
|
418
|
+
status: result.status,
|
|
419
|
+
summary: {
|
|
420
|
+
errors: result.errors,
|
|
421
|
+
warnings: result.warnings,
|
|
422
|
+
notices: result.notices,
|
|
423
|
+
infos: result.infos,
|
|
424
|
+
},
|
|
425
|
+
issues: result.issues.map(mapIssueToJson),
|
|
426
|
+
stats: toStatsJson(result.metadata, options),
|
|
427
|
+
})),
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
const mapIssueToJson = (issue) => {
|
|
431
|
+
const hasLocation = typeof issue.line === 'number' &&
|
|
432
|
+
typeof issue.column === 'number' &&
|
|
433
|
+
issue.line > 0 &&
|
|
434
|
+
issue.column > 0;
|
|
435
|
+
return {
|
|
436
|
+
severity: issue.severity,
|
|
437
|
+
code: issue.code,
|
|
438
|
+
message: issue.message,
|
|
439
|
+
location: hasLocation
|
|
440
|
+
? {
|
|
441
|
+
line: issue.line,
|
|
442
|
+
column: issue.column,
|
|
443
|
+
}
|
|
444
|
+
: undefined,
|
|
445
|
+
advice: issue.advice,
|
|
446
|
+
hint: issue.hint,
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
const toStatsJson = (metadata, options) => {
|
|
450
|
+
if (!options.stats || !metadata) {
|
|
451
|
+
return undefined;
|
|
333
452
|
}
|
|
334
|
-
|
|
335
|
-
|
|
453
|
+
return {
|
|
454
|
+
lineCount: metadata.source.lineCount,
|
|
455
|
+
byteSize: metadata.source.sourceType === 'inline'
|
|
456
|
+
? null
|
|
457
|
+
: metadata.source.byteSize,
|
|
458
|
+
sectionCount: metadata.structure.sectionCount,
|
|
459
|
+
memberCount: metadata.structure.memberCount,
|
|
460
|
+
nestingDepth: metadata.structure.maxDepth,
|
|
461
|
+
hasYiniMarker: metadata.source.hasYiniMarker,
|
|
462
|
+
hasDocumentTerminator: metadata.source.hasDocumentTerminator,
|
|
463
|
+
};
|
|
336
464
|
};
|
|
337
|
-
// ---
|
|
338
|
-
|
|
339
|
-
/*
|
|
340
|
-
- Produce a summary-level validation report.
|
|
341
|
-
- Output is structured and concise (e.g. JSON or table-like).
|
|
342
|
-
- Focus on counts, pass/fail, severity summary.
|
|
343
|
-
|
|
344
|
-
Example:
|
|
345
|
-
Validation report for config.yini:
|
|
346
|
-
Errors: 3
|
|
347
|
-
Warnings: 1
|
|
348
|
-
Notices: 0
|
|
349
|
-
Result: INVALID
|
|
350
|
-
*/
|
|
351
|
-
const formatToStatsReport = (fileWithPath, metadata) => {
|
|
352
|
-
// console.log('formatToStatsReport(..)')
|
|
353
|
-
// printObject(metadata)
|
|
354
|
-
// console.log()
|
|
465
|
+
// --- Stats -------------------------------------------------------------------
|
|
466
|
+
const formatStatsReport = (metadata) => {
|
|
355
467
|
if (!metadata?.diagnostics) {
|
|
356
|
-
|
|
357
|
-
exit(1);
|
|
468
|
+
throw new Error('Internal error: Missing diagnostics metadata.');
|
|
358
469
|
}
|
|
359
470
|
const diag = metadata.diagnostics;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const str = `Validation Report
|
|
365
|
-
=================
|
|
366
|
-
|
|
367
|
-
File "${fileWithPath}"
|
|
368
|
-
Issues: ${issuesCount}
|
|
369
|
-
|
|
370
|
-
Summary
|
|
371
|
-
-------
|
|
372
|
-
Mode: ${metadata.mode}
|
|
373
|
-
Strict: ${metadata.mode === 'strict'}
|
|
374
|
-
|
|
375
|
-
Errors: ${diag.errors.errorCount}
|
|
376
|
-
Warnings: ${diag.warnings.warningCount}
|
|
377
|
-
Notices: ${diag.notices.noticeCount}
|
|
378
|
-
Infos: ${diag.infos.infoCount}
|
|
379
|
-
|
|
380
|
-
Stats
|
|
381
|
-
-----
|
|
471
|
+
return `Statistics
|
|
472
|
+
----------
|
|
473
|
+
Notices: ${diag.notices.noticeCount}
|
|
474
|
+
Infos: ${diag.infos.infoCount}
|
|
382
475
|
Line Count: ${metadata.source.lineCount}
|
|
383
476
|
Section Count: ${metadata.structure.sectionCount}
|
|
384
477
|
Member Count: ${metadata.structure.memberCount}
|
|
385
478
|
Nesting Depth: ${metadata.structure.maxDepth}
|
|
386
|
-
|
|
387
479
|
Has @YINI: ${metadata.source.hasYiniMarker}
|
|
388
480
|
Has /END: ${metadata.source.hasDocumentTerminator}
|
|
389
|
-
Byte Size: ${metadata.source.sourceType === 'inline'
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
393
|
-
// -------------------------------------------------------------------------
|
|
394
|
-
// --- Format to a Details --------------------------------------------------------
|
|
395
|
-
//@todo format parsed.meta to details as
|
|
396
|
-
/*
|
|
397
|
-
- Show full detailed validation messages.
|
|
398
|
-
- Output includes line numbers, columns, error codes, and descriptive text.
|
|
399
|
-
- Useful for debugging YINI files.
|
|
400
|
-
|
|
401
|
-
Example:
|
|
402
|
-
Error at line 5, column 9: Unexpected '/END' — expected <EOF>
|
|
403
|
-
Warning at line 10, column 3: Section level skipped (0 → 2)
|
|
404
|
-
Notice at line 1: Unused @yini directive
|
|
405
|
-
*/
|
|
406
|
-
const printIssuesFound = (fileWithPath, metadata) => {
|
|
407
|
-
// console.log('printDetails(..)')
|
|
408
|
-
// printObject(metadata)
|
|
409
|
-
// console.log(toPrettyJSON(metadata))
|
|
410
|
-
// console.log()
|
|
411
|
-
if (!metadata?.diagnostics) {
|
|
412
|
-
console.error('Internal error: Missing diagnostics metadata');
|
|
413
|
-
exit(1);
|
|
414
|
-
}
|
|
415
|
-
const diag = metadata.diagnostics;
|
|
416
|
-
IS_DEBUG && console.log('*** Issues Found');
|
|
417
|
-
IS_DEBUG && console.log('*** ------------');
|
|
418
|
-
IS_DEBUG && console.log();
|
|
419
|
-
const errors = diag.errors.payload;
|
|
420
|
-
printIssues('Error ', 'E', errors);
|
|
421
|
-
const warnings = diag.warnings.payload;
|
|
422
|
-
printIssues('Warning', 'W', warnings);
|
|
423
|
-
const notices = diag.notices.payload;
|
|
424
|
-
printIssues('Notice ', 'N', notices);
|
|
425
|
-
const infos = diag.infos.payload;
|
|
426
|
-
printIssues('Info ', 'I', infos);
|
|
427
|
-
return;
|
|
428
|
-
};
|
|
429
|
-
// -------------------------------------------------------------------------
|
|
430
|
-
const printIssues = (typeLabel, prefix, issues) => {
|
|
431
|
-
const leftPadding = ' ';
|
|
432
|
-
issues.forEach((iss, i) => {
|
|
433
|
-
const id = '#' + prefix + '-0' + (i + 1);
|
|
434
|
-
// const id: string = '' + prefix + '-0' + (i+1) + ':'
|
|
435
|
-
let str = `${typeLabel} [${id}]:\n`;
|
|
436
|
-
str +=
|
|
437
|
-
leftPadding +
|
|
438
|
-
`At line ${iss.line}, column ${iss.column}: ${iss.message}`;
|
|
439
|
-
if (iss.advice)
|
|
440
|
-
str += '\n' + leftPadding + iss.advice;
|
|
441
|
-
if (iss.hint)
|
|
442
|
-
str += '\n' + leftPadding + iss.hint;
|
|
443
|
-
console.log(str);
|
|
444
|
-
console.log();
|
|
445
|
-
});
|
|
481
|
+
Byte Size: ${metadata.source.sourceType === 'inline'
|
|
482
|
+
? 'n/a'
|
|
483
|
+
: `${metadata.source.byteSize} bytes`}`;
|
|
446
484
|
};
|