-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathindex.js
291 lines (263 loc) · 10.6 KB
/
index.js
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
284
285
286
287
288
289
290
291
"use strict";
const fs = require("fs");
const { detailedDiff } = require("deep-object-diff");
const chalk = require("chalk");
const emoji = require("node-emoji");
const warning = emoji.get("warning");
const fire = emoji.get("fire");
const cwd = process.cwd();
const path = require("path");
const tableFormatter = require("eslint-formatter-table");
module.exports = function (results, context, logger = console) {
const defaultExitZero = process.env.RATCHET_DEFAULT_EXIT_ZERO === "true";
const filesLinted = [];
const latestIssues = {};
// Get previous/latest warning/error counts overall and group them per file/rule
let previousIssues = {};
if (fs.existsSync("./eslint-ratchet.json")) {
previousIssues = JSON.parse(fs.readFileSync("./eslint-ratchet.json"));
}
// Loop over results and store them as file/rule/issueType:count. Ex:
// Ex:
// {
// "some/file.js": {
// "an-eslint-rule": {
// "warning": 1,
// "error": 2
// }
// }
// }
let logResults = false;
results.forEach(({ messages, filePath, errorCount, warningCount }) => {
const file = path.relative(cwd, filePath);
filesLinted.push(file);
if (errorCount > 0 || warningCount > 0) {
logResults = true;
latestIssues[file] = {};
messages.forEach(({ ruleId, severity }) => {
latestIssues[file][ruleId] = latestIssues[file][ruleId] || {
warning: 0,
error: 0,
};
latestIssues[file][ruleId].warning += severity === 1 ? 1 : 0;
latestIssues[file][ruleId].error += severity === 2 ? 1 : 0;
});
}
});
if (logResults) {
// Use the default table formatter to post the results
// Since Eslint expects to only be dealing with a single formatter we can wind up in a case where an error is thrown due to
// a violation but this formatter is only concerned with ratcheting and effectively eats the details. To prevent this from
// happening we'll now log results via the table formatter so that issues are always exposed.
logger.log(tableFormatter(results));
}
// Store these latest results up front.
// These are mentioned in the logging whenever counts increase and allow for easy updating
// when those increases were expected.
fs.writeFileSync(
"./eslint-ratchet-temp.json",
JSON.stringify({ ...previousIssues, ...latestIssues }, null, 4),
);
// Perform a basic check to see if anything has changed
const diff = detailedDiff(previousIssues, latestIssues);
const { added, updated, deleted } = diff;
// Filter results to just those that are for the linted files.
// Since the latest issues will have a diff of `undefined` all we need to do is
// filter the results to those that aren't null/undefined
const addsMatchingLintedFiles = Object.entries(added).filter(([k, v]) =>
filesLinted.includes(k),
);
const updatesMatchingLintedFiles = Object.entries(updated).filter(([k, v]) =>
filesLinted.includes(k),
);
const deletesMatchingLintedFiles = Object.entries(deleted).filter(([k, v]) =>
filesLinted.includes(k),
);
// Also look for files that were previously linted but no longer exist.
// This helps account for times when linting may only be performed against a subset of files
// but one or more of the previous files has been removed.
const missingFiles = Object.keys(previousIssues).filter(
(filepath) => !fs.existsSync(filepath),
);
const hasChanged =
missingFiles.length != 0 ||
addsMatchingLintedFiles.length != 0 ||
updatesMatchingLintedFiles.length != 0 ||
deletesMatchingLintedFiles.length != 0;
// If there are changes find/log/save differences
if (hasChanged) {
logger.group(
chalk.yellow(
warning,
` eslint-ratchet: Changes to eslint results detected!!!`,
),
);
// Loop over the changes to determine and log what's different
const { newIssues, updatedResults } = detectAndLogChanges(
previousIssues,
filesLinted,
added,
updated,
deleted,
logger,
);
// If we find any "issues" (increased/new counts) throw a warning and fail the ratcheting check
if (newIssues > 0) {
logger.log(
fire,
chalk.red(` New eslint-ratchet issues have been detected!!!`),
);
logger.log(
`These latest eslint results have been saved to ${chalk.yellow.underline(
"eslint-ratchet-temp.json",
)}. \nIf these results were expected then use them to replace the content of ${chalk.white.underline(
"eslint-ratchet.json",
)} and check it in.`,
);
throw new Error("View output above for more details");
} else {
// Otherwise update the ratchet tracking and log a message about it
fs.writeFileSync(
"./eslint-ratchet.json",
JSON.stringify(updatedResults, null, 4),
);
fs.writeFileSync(
"./eslint-ratchet-temp.json",
JSON.stringify({}, null, 4),
);
return chalk.green(
`Changes found are all improvements! These new results have been saved to ${chalk.white.underline(
"eslint-ratchet.json",
)}`,
);
}
}
// If there is any rule violation of type "error", eslint will exit non-zero.
// Since we're ratcheting though chances are we already have errors - we just don't want new ones.
// To get around eslint's default behavior but also not stray too far from it we'll check an env var to
// determine if we should bypass that behavior and instead exit will 0.
if (defaultExitZero) {
logger.log("eslint-ratchet: causing process to exit 0");
process.exit(0);
}
// Because eslint expects a string response from formatters, but our messaging is already complete, just
// return an empty string.
return "";
};
// Log the results of a change based on the type of change.
const logColorfulValue = (violationType, value, previously, color, logger) => {
logger.log(
`--> ${violationType}: ${chalk[color](value)} (previously: ${chalk.yellow(
previously,
)})`,
);
};
// Loop over the latest results and detect changes within each type.
// In cases where any change is detected it is logged with the previous result and color coded
const detectAndLogChanges = (
previousResults,
filesLinted,
added,
updated,
deleted,
logger,
) => {
// Keep track of any new issues - where the counts for a previously reported
// issue have gone up
let newIssues = 0;
const updatedResults = Object.assign({}, previousResults);
Object.entries({ added, updated, deleted }).forEach(([setKey, set]) => {
Object.entries(set).forEach(([fileKey, fileValue]) => {
// Only check against files that were linted in the latest run.
const fileLinted = filesLinted.includes(fileKey);
if (!fileLinted) {
// Check to see if the file wasn't linted because it no longer exists
// If it does exist then it wasn't a part of the latest run, like when running against staged files,
// and its previous results are safe to ignored.
// If it doesn't exist then any error counts associated with it should be removed and are accounted
// for later on.
const exists = fs.existsSync(fileKey);
if (exists) return;
}
logger.group(chalk.white.underline(fileKey));
let previousFileResults = previousResults[fileKey];
// For our "deleted" issues, or issues that we've fixed that no longer reported,
// there is nothing new to compare against so instead create an empty case.
if (!fileValue && setKey === "deleted") {
fileValue = {};
Object.keys(previousFileResults).forEach((key) => {
fileValue[key] = { warning: 0, error: 0 };
});
}
// Check if the new value for each rule/result is higher (worse) or lower (better) than before
Object.entries(fileValue).forEach(([rule, result]) => {
logger.group(rule);
// If the issue is no longer valid log and remove it.
// Removal at this stage only applies to cases where there are no longer any issues.
if (!result) {
logger.log(`--> ${chalk.green("all issues resolved")}`);
if (updatedResults[fileKey][rule])
delete updatedResults[fileKey][rule];
} else if (result) {
Object.entries(result).forEach(([violationType, value]) => {
// Fill in missing rules when entirely new cases are added.
// This can happen in multiple cases, like:
// - when a new file is added but has issues
// - when a linter's rules are changed and it now reports more issues
// - when a new linter is added and begins reporting on new issues
if (!previousFileResults && setKey === "added") {
previousFileResults = {};
}
let previousRule = previousFileResults[rule];
if (!previousRule && setKey === "added") {
previousRule = { warning: 0, error: 0 };
}
const previousValue = previousRule[violationType];
//Report the change and track if new issues have occurred
if (value > previousValue) {
newIssues += 1;
logColorfulValue(
violationType,
value,
previousValue,
"red",
logger,
);
} else if (value < previousValue) {
logColorfulValue(
violationType,
value,
previousValue,
"green",
logger,
);
}
// Set the updated value for the violation
updatedResults[fileKey] = updatedResults[fileKey] || {};
updatedResults[fileKey][rule] = updatedResults[fileKey][rule] || {};
updatedResults[fileKey][rule][violationType] = value;
});
}
// Clean up results where issues have been fixed
// Remove 0 counts - no need to track them
if (result?.warning === 0) delete updatedResults[fileKey][rule].warning;
if (result?.error === 0) delete updatedResults[fileKey][rule].error;
// Remove rules without issues
if (updatedResults[fileKey] && updatedResults[fileKey][rule]) {
if (Object.keys(updatedResults[fileKey][rule]).length === 0) {
delete updatedResults[fileKey][rule];
}
}
// Remove files without issues
if (updatedResults[fileKey]) {
if (Object.keys(updatedResults[fileKey]).length === 0) {
delete updatedResults[fileKey];
}
}
logger.groupEnd();
});
logger.groupEnd();
});
});
return { newIssues, updatedResults };
};