-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathValidationError.js
More file actions
283 lines (245 loc) · 7.82 KB
/
ValidationError.js
File metadata and controls
283 lines (245 loc) · 7.82 KB
1
2
3
4
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
import chalk from "chalk";
import escapeStringRegExp from "escape-string-regexp";
/**
* Error class for validation of project configuration.
*
* @public
* @class
* @alias @ui5/project/validation/ValidationError
* @extends Error
* @hideconstructor
*/
class ValidationError extends Error {
constructor({errors, project, yaml}) {
super();
/**
* ValidationError
*
* @constant
* @default
* @type {string}
* @readonly
* @public
*/
this.name = "ValidationError";
this.project = project;
this.yaml = yaml;
this.errors = ValidationError.filterErrors(errors);
/**
* Formatted error message
*
* @type {string}
* @readonly
* @public
*/
this.message = this.formatErrors();
Error.captureStackTrace(this, this.constructor);
}
formatErrors() {
let separator = "\n\n";
if (process.stdout.isTTY) {
// Add a horizontal separator line between errors in case a terminal is used
separator += chalk.grey.dim("\u2500".repeat(process.stdout.columns || 80));
}
separator += "\n\n";
let message;
if (this.project) { // ui5-workspace.yaml is project independent, so in that case, no project is available
message = chalk.red(`Invalid ui5.yaml configuration for project ${this.project.id}`) + "\n\n";
} else {
message = chalk.red(`Invalid workspace configuration.`) + "\n\n";
}
message += this.errors.map((error) => {
return this.formatError(error);
}).join(separator);
return message;
}
formatError(error) {
let errorMessage = ValidationError.formatMessage(error);
if (this.yaml && this.yaml.path && this.yaml.source) {
const yamlExtract = ValidationError.getYamlExtract({error, yaml: this.yaml});
const errorLines = errorMessage.split("\n");
errorLines.splice(1, 0, "\n" + yamlExtract);
errorMessage = errorLines.join("\n");
}
return errorMessage;
}
static formatMessage(error) {
if (error.keyword === "errorMessage") {
return error.message;
}
let message = "Configuration ";
if (error.instancePath) {
message += chalk.underline(chalk.red(error.instancePath.substr(1))) + " ";
}
switch (error.keyword) {
case "additionalProperties":
message += `property ${error.params.additionalProperty} must not be provided here`;
break;
case "type":
message += `must be of type '${error.params.type}'`;
break;
case "required":
message += `must have required property '${error.params.missingProperty}'`;
break;
case "enum":
message += "must be equal to one of the allowed values\n";
message += "Allowed values: " + error.params.allowedValues.join(", ");
break;
default:
message += error.message;
}
return message;
}
static _findDuplicateError(error, errorIndex, errors) {
const foundIndex = errors.findIndex(($) => {
if ($.instancePath !== error.instancePath) {
return false;
} else if ($.keyword !== error.keyword) {
return false;
} else if (JSON.stringify($.params) !== JSON.stringify(error.params)) {
return false;
} else {
return true;
}
});
return foundIndex !== errorIndex;
}
static filterErrors(allErrors) {
return allErrors.filter((error, i, errors) => {
if (error.keyword === "if" || error.keyword === "oneOf") {
return false;
}
return !ValidationError._findDuplicateError(error, i, errors);
});
}
static analyzeYamlError({error, yaml}) {
if (error.instancePath === "" && error.keyword === "required") {
// There is no line/column for a missing required property on root level
return {line: -1, column: -1};
}
// Skip leading /
const objectPath = error.instancePath.substr(1).split("/");
if (error.keyword === "additionalProperties") {
objectPath.push(error.params.additionalProperty);
}
let currentSubstring;
let currentIndex;
if (yaml.documentIndex) {
const matchDocumentSeparator = /^---/gm;
let currentDocumentIndex = 0;
let document;
while ((document = matchDocumentSeparator.exec(yaml.source)) !== null) {
// If the first separator is not at the beginning of the file
// we are already at document index 1
// Using String#trim() to remove any whitespace characters
if (currentDocumentIndex === 0 && yaml.source.substring(0, document.index).trim().length > 0) {
currentDocumentIndex = 1;
}
if (currentDocumentIndex === yaml.documentIndex) {
currentIndex = document.index;
currentSubstring = yaml.source.substring(currentIndex);
break;
}
currentDocumentIndex++;
}
// Document could not be found
if (!currentSubstring) {
return {line: -1, column: -1};
}
} else {
// In case of index 0 or no index, use whole source
currentIndex = 0;
currentSubstring = yaml.source;
}
const matchArrayElementIndentation = /([ ]*)-/;
for (let i = 0; i < objectPath.length; i++) {
const property = objectPath[i];
let newIndex;
if (isNaN(property)) {
// Try to find a property
// Creating a regular expression that matches the property name a line
// except for comments, indicated by a hash sign "#".
const propertyRegExp = new RegExp(`^[^#]*?${escapeStringRegExp(property)}`, "m");
const propertyMatch = propertyRegExp.exec(currentSubstring);
if (!propertyMatch) {
return {line: -1, column: -1};
}
newIndex = propertyMatch.index + propertyMatch[0].length;
} else {
// Try to find the right index within an array definition.
// This currently only works for arrays defined with "-" in multiple lines.
// Arrays using square brackets are not supported.
const matchArrayElement = /(^|\r?\n)([ ]*-[^\r\n]*)/g;
const arrayIndex = parseInt(property);
let a = 0;
let firstIndentation = -1;
let match;
while ((match = matchArrayElement.exec(currentSubstring)) !== null) {
const indentationMatch = match[2].match(matchArrayElementIndentation);
if (!indentationMatch) {
return {line: -1, column: -1};
}
const currentIndentation = indentationMatch[1].length;
if (firstIndentation === -1) {
firstIndentation = currentIndentation;
} else if (currentIndentation !== firstIndentation) {
continue;
}
if (a === arrayIndex) {
// match[1] might be a line-break
newIndex = match.index + match[1].length + currentIndentation;
break;
}
a++;
}
if (!newIndex) {
// Could not find array element
return {line: -1, column: -1};
}
}
currentIndex += newIndex;
currentSubstring = yaml.source.substring(currentIndex);
}
const linesUntilMatch = yaml.source.substring(0, currentIndex).split(/\r?\n/);
const line = linesUntilMatch.length;
let column = linesUntilMatch[line - 1].length + 1;
const lastPathSegment = objectPath[objectPath.length - 1];
if (isNaN(lastPathSegment)) {
column -= lastPathSegment.length;
}
return {
line,
column
};
}
static getSourceExtract(yamlSource, line, column) {
let source = "";
const lines = yamlSource.split(/\r?\n/);
// Using line numbers instead of array indices
const startLine = Math.max(line - 2, 1);
const endLine = Math.min(line, lines.length);
const padLength = String(endLine).length;
for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
const currentLineContent = lines[currentLine - 1];
let string = chalk.gray(
String(currentLine).padStart(padLength, " ") + ":"
) + " " + currentLineContent + "\n";
if (currentLine === line) {
string = chalk.bgRed(string);
}
source += string;
}
source += " ".repeat(column + padLength + 1) + chalk.red("^");
return source;
}
static getYamlExtract({error, yaml}) {
const {line, column} = ValidationError.analyzeYamlError({error, yaml});
if (line !== -1 && column !== -1) {
return chalk.grey(yaml.path + ":" + line) +
"\n\n" + ValidationError.getSourceExtract(yaml.source, line, column);
} else {
return chalk.grey(yaml.path) + "\n";
}
}
}
export default ValidationError;