forked from OverlayPlugin/cactbot
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathupdate_logdefs.ts
444 lines (379 loc) · 15.1 KB
/
update_logdefs.ts
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import * as core from '@actions/core';
import logDefinitions, { LogDefinitionName } from '../resources/netlog_defs';
import { LooseOopsyTriggerSet } from '../types/oopsy';
import { LooseTriggerSet } from '../types/trigger';
import { TimelineParser } from '../ui/raidboss/timeline_parser';
import { walkDirSync } from './file_utils';
// This script parses all raidboss/oopsy triggers and timelines, finds log line types used in them,
// and compares against `netlog_defs.ts` to find any types that are not presently being included
// in the log splitter's analysis filter (based on the `analysisOptions.include` property).
// If the property is absent, this script will create it and set it to 'all'.
// If the type should be ignored by this script (despite being used), `include` can instead be set
// to 'never'. Alternatively, set the type to 'filter' if only certain lines of that type should be
// included in the analysis filter. See `netlog_defs.ts` for more information.
// This script can be run via CLI as `npm run update-logdefs`. If run via GitHub Actions (after a
// triggering merge commit), the workflow will automatically create a PR to merge any changes.
const isGithubRunner = process.env.GITHUB_ACTIONS === 'true';
const sha = process.env.GITHUB_SHA ?? 'main';
const repo = process.env.GITHUB_REPOSITORY ?? 'OverlayPlugin/cactbot';
const baseUrl = `https://github.com/${repo}/blob`;
const raidbossRelDir = 'ui/raidboss/data';
const oopsyRelDir = 'ui/oopsyraidsy/data';
const netLogDefsFile = 'resources/netlog_defs.ts';
type FileList = {
timelines: string[];
triggers: string[];
oopsy: string[];
};
type FileMatch = {
filename: string;
excerptStartLine: number;
excerptStopLine?: number;
};
type FileMatches = Partial<Record<LogDefinitionName, FileMatch[]>>;
class TimelineTypeExtractor extends TimelineParser {
public entries: Partial<Record<LogDefinitionName, number[]>> = {};
constructor(contents: string) {
// construct parent with waitForParse = true, because `entries` is initialized
// after parent construction, but is populated by parse() method in parent
super(contents, [], [], undefined, undefined, undefined, true);
// we've initialized entries now, so call parse()
this.parse(contents, [], [], 0);
}
public override parseType(type: LogDefinitionName, lineNumber: number) {
(this.entries[type] ??= []).push(lineNumber);
}
}
class LogDefUpdater {
private scriptFile = '';
private projectRoot = '';
private fileList: FileList;
// List of log line names that do not have any analysisOptions in netlog_defs
private logDefsNoInclude: LogDefinitionName[] = [];
// List of log line names that have analysisOptions.include = 'never'
// We don't update these, but collect usage so we can console.log() a notice about it
private logDefsNeverInclude: LogDefinitionName[] = [];
// Matches of non-included log line types found in triggers & timelines
private matches: FileMatches = {};
// List of log line names that are being added to the analysis filter
private logDefsToUpdate: LogDefinitionName[] = [];
constructor() {
this.scriptFile = fileURLToPath(import.meta.url);
this.projectRoot = path.resolve(path.dirname(this.scriptFile), '..');
this.logDefsNoInclude = Object.values(logDefinitions).filter((def) =>
!('analysisOptions' in def)
).map((def) => def.name);
this.logDefsNeverInclude = Object.values(logDefinitions).filter((def) =>
('analysisOptions' in def) && def.analysisOptions.include === 'never'
).map((def) => def.name);
this.fileList = this.getFileList();
}
isLogDefinitionName(type: string | undefined): type is LogDefinitionName {
return type !== undefined && type in logDefinitions;
}
buildRefUrl(file: string, sha: string, startLine: number, stopLine?: number): string {
return stopLine
? `${baseUrl}/${sha}/${file}#L${startLine}-L${stopLine}`
: `${baseUrl}/${sha}/${file}#L${startLine}`;
}
buildPullRequestBodyContent(): string {
let output = '';
for (const type of this.logDefsNoInclude) {
const matches = this.matches[type] ?? [];
if (matches.length === 0)
continue;
output += `\n## \`${type}\`\n`;
matches.forEach((m) => {
output += `${this.buildRefUrl(m.filename, sha, m.excerptStartLine, m.excerptStopLine)}\n`;
});
}
return output;
}
processAndLogResults(): void {
// log results to the console for both CLI & GH workflow execution
for (const type of this.logDefsNoInclude) {
const matches = this.matches[type];
if (matches === undefined || matches.length === 0)
continue;
console.log(`** ${type} **`);
console.log(`Found non-included log line type in active use:`);
matches.forEach((m) => {
console.log(` - ${m.filename}:${m.excerptStartLine}`);
});
console.log(`LOG DEFS UPDATED: ${type} is being added to the analysis filter.\n`);
this.logDefsToUpdate.push(type);
}
// Log a notice for 'never' log line types, just so we're aware of the usage count for each.
// In theory, these are set to 'never' because we really don't care about them for analysis,
// but a periodic reminder to re-evaluate never hurts.
for (const type of this.logDefsNeverInclude) {
const numMatches = (this.matches[type]?.length ?? 0);
if (numMatches > 0) {
console.log(`** ${type} **`);
console.log(
`Found ${numMatches} active use(s) of suppressed ('never') log line type.`,
);
console.log(
`${type} will not be added to the analysis filter, but please consider whether updates are needed.\n`,
);
}
}
}
getFileList(): FileList {
const fileList: FileList = {
timelines: [],
triggers: [],
oopsy: [],
};
walkDirSync(path.posix.join(this.projectRoot, raidbossRelDir), (filepath) => {
if (/\/raidboss_manifest.txt/.test(filepath)) {
return;
}
if (/\/raidboss\/data\/.*\.txt/.test(filepath)) {
fileList.timelines.push(filepath);
return;
}
if (/\/raidboss\/data\/.*\.[jt]s/.test(filepath)) {
fileList.triggers.push(filepath);
return;
}
});
walkDirSync(path.posix.join(this.projectRoot, oopsyRelDir), (filepath) => {
if (/\/oopsy_manifest.txt/.test(filepath)) {
return;
}
if (/\/oopsyraidsy\/data\/.*\.[jt]s/.test(filepath)) {
fileList.oopsy.push(filepath);
return;
}
});
return fileList;
}
async parseTriggerFile(file: string): Promise<void> {
// Normalize path
const importPath = `../${path.relative(process.cwd(), file).replace('.ts', '.js')}`;
// Dynamic imports don't have a type, so add type assertion.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const triggerSet = (await import(importPath)).default as LooseTriggerSet;
const triggerSetArr = triggerSet.triggers?.entries();
if (triggerSetArr === undefined)
console.error(`ERROR: Could not find triggers in ${file}`);
const contents = fs.readFileSync(file).toString();
const lines = contents.split(/\r*\n/);
for (const [index, trigger] of triggerSet.triggers?.entries() ?? []) {
const id = trigger.id;
const type: string | undefined = trigger.type; // override literal type from LooseTrigger
let lineNum = 0;
let idLine = 0;
let regexLine = 0;
if (id === undefined) {
console.error(`ERROR: Missing trigger id property in ${file} (trigger index: ${index})`);
continue;
} else if (type === undefined) {
console.error(`ERROR: Missing trigger type property for trigger '${id}' in ${file}`);
continue;
}
const escapedId = id.replace(/'/g, '\\\'');
for (const line of lines) {
++lineNum;
if (line.includes(`id: '${escapedId}',`)) {
// if we match an id line with one already set, we never found a regex line;
// in that case exit the loop & report the error
if (idLine === 0)
idLine = lineNum;
else
break;
} else if (idLine > 0 && line.includes('netRegex: {')) {
regexLine = lineNum;
break;
}
}
if (idLine === 0) {
console.error(`ERROR: Could not find trigger '${id}' in ${file}`);
continue;
} else if (regexLine === 0) {
console.error(`ERROR: Could not find netRegex for trigger '${id}' in ${file}`);
continue;
}
if (!this.isLogDefinitionName(type)) {
console.error(`ERROR: Missing log def for ${type} in ${file} (line: ${idLine})`);
continue;
} else if (
this.logDefsNoInclude.includes(type) ||
this.logDefsNeverInclude.includes(type)
)
(this.matches[type] ??= []).push({
filename: file.replace(`${this.projectRoot}/`, ''),
excerptStartLine: idLine,
excerptStopLine: regexLine,
});
}
}
async parseOopsyFile(file: string): Promise<void> {
// Oopsy files do not need to have triggers, and their triggers do not need to have types.
// So this method is a lot more permissive than parseTriggerFile().
// Normalize path
const importPath = `../${path.relative(process.cwd(), file).replace('.ts', '.js')}`;
// Dynamic imports don't have a type, so add type assertion.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const triggerSet = (await import(importPath)).default as LooseOopsyTriggerSet;
const contents = fs.readFileSync(file).toString();
const lines = contents.split(/\r*\n/);
for (const trigger of triggerSet.triggers ?? []) {
const id = trigger.id;
const type: string | undefined = trigger.type; // override literal type from LooseTrigger
let lineNum = 0;
let idLine = 0;
let regexLine: number | undefined = 0;
if (id === undefined || type === undefined)
continue;
const escapedId = id.replace(/'/g, '\\\'');
for (const line of lines) {
++lineNum;
if (line.includes(`id: '${escapedId}',`)) {
// if we match an id line with one already set, we never found a regex line;
// in that case exit the loop & move on
if (idLine === 0)
idLine = lineNum;
else
break;
} else if (idLine > 0 && line.includes('netRegex: {')) {
regexLine = lineNum;
break;
}
}
if (idLine === 0) {
// Might not have been able to find an id: line because the trigger may be
// generated dynamically - see aloalo_island. In that case, just string-search
// for the naked id by itself (it has to be there somewhere).
lineNum = 0;
for (const line of lines) {
++lineNum;
if (line.includes(`'${escapedId}'`)) {
idLine = lineNum;
break;
}
}
if (idLine === 0) {
console.error(`ERROR: Could not find trigger '${id}' in ${file}`);
continue;
} else
regexLine = undefined; // don't try to add lines to the excerpt
}
// if we found the id but not the regex, just capture the two lines after the id line
regexLine = regexLine === 0 ? idLine + 2 : regexLine;
if (!this.isLogDefinitionName(type)) {
console.error(`ERROR: Missing log def for ${type} in ${file} (line: ${idLine})`);
continue;
} else if (
this.logDefsNoInclude.includes(type) ||
this.logDefsNeverInclude.includes(type)
)
(this.matches[type] ??= []).push({
filename: file.replace(`${this.projectRoot}/`, ''),
excerptStartLine: idLine,
excerptStopLine: regexLine,
});
}
}
parseTimelineFile(file: string): void {
const contents = fs.readFileSync(file).toString();
const entries = new TimelineTypeExtractor(contents).entries;
if (entries === undefined) {
console.error(`ERROR: Could not find timeline sync entries in ${file}`);
return;
}
for (const [type, lineNums] of Object.entries(entries)) {
if (!this.isLogDefinitionName(type))
console.error(
`ERROR: Missing log def for ${type} in ${file} (line: ${lineNums[0] ?? '?'})`,
);
else if (
this.logDefsNoInclude.includes(type) ||
this.logDefsNeverInclude.includes(type)
) {
for (const lineNum of lineNums) {
(this.matches[type] ??= []).push({
filename: file.replace(`${this.projectRoot}/`, ''),
excerptStartLine: lineNum,
});
}
}
}
}
updateNetLogDefsFile(): void {
if (this.logDefsNoInclude.length === 0)
return;
const contents = fs.readFileSync(path.posix.join(this.projectRoot, netLogDefsFile)).toString();
const lines = contents.split(/\r*\n/);
const fileRegex = {
inConst: /^const latestLogDefinitions = {/,
inLogDef: /^ {2}(\w+): \{/,
outLogDef: /^ {2}\},/,
outConst: /^} as const;/,
};
const output: string[] = [];
let foundConst = false;
let insideConst = false;
let insideLogDef = false;
let updateThisLogDef = false;
for (const line of lines) {
// initial processing - haven't found the logdefs yet
if (!foundConst) {
if (line.match(fileRegex.inConst)) {
foundConst = true;
insideConst = true;
}
output.push(line);
continue;
}
// we're done updating, so just write the rest of the file
if (!insideConst) {
output.push(line);
continue;
}
// looking for the next logdef
if (!insideLogDef) {
const logDefName = line.match(fileRegex.inLogDef)?.[1];
if (logDefName !== undefined && this.isLogDefinitionName(logDefName)) {
insideLogDef = true;
if (this.logDefsToUpdate.includes(logDefName))
updateThisLogDef = true;
}
} else if (line.match(fileRegex.outLogDef)) {
// at the end of the logdef; update it now if needed
insideLogDef = false;
if (updateThisLogDef) {
const objToAdd = ` analysisOptions: {\r\n include: 'all',\r\n },`;
output.push(objToAdd);
updateThisLogDef = false;
}
} else if (insideConst && line.match(fileRegex.outConst))
insideConst = false;
output.push(line);
}
fs.writeFileSync(path.posix.join(this.projectRoot, netLogDefsFile), output.join('\r\n'));
}
async doUpdate(): Promise<void> {
console.log('Processing trigger files...');
for (const f of this.fileList.triggers) {
await this.parseTriggerFile(f);
}
console.log('Processing oopsy files...');
for (const f of this.fileList.oopsy) {
await this.parseOopsyFile(f);
}
console.log('Processing timeline files...');
this.fileList.timelines.forEach((f) => this.parseTimelineFile(f));
console.log('File processing complete.\r\n');
this.processAndLogResults();
this.updateNetLogDefsFile();
if (isGithubRunner)
core.setOutput('changelist', this.buildPullRequestBodyContent());
}
}
const updater = new LogDefUpdater();
await updater.doUpdate();