UNPKG

@plugjs/plug

Version:
346 lines 14.3 kB
import { BuildFailure } from "../asserts.js"; import { readFile } from "../fs.js"; import { $blu, $cyn, $gry, $plur, $red, $und, $wht, $ylw } from "./colors.js"; import { githubAnnotation } from "./github.js"; import { ERROR, NOTICE, WARN, logLevels } from "./levels.js"; import { logOptions } from "./options.js"; let _showSources = logOptions.showSources; let _githubAnnotations = logOptions.githubAnnotations; logOptions.on('changed', (options) => { _showSources = options.showSources; _githubAnnotations = options.githubAnnotations; }); /* ========================================================================== * * REPORT IMPLEMENTATION * * ========================================================================== */ const nul = '\u2400'; // null, yep, as a character, always gets sorted last export class ReportImpl { _title; _task; _emitter; _sources = new Map(); _annotations = new Map(); _records = new Map(); _noticeRecords = 0; _warningRecords = 0; _errorRecords = 0; _noticeAnnotations = 0; _warningAnnotations = 0; _errorAnnotations = 0; constructor(_title, _task, _emitter) { this._title = _title; this._task = _task; this._emitter = _emitter; } get notices() { return this._noticeRecords + this._noticeAnnotations; } get warnings() { return this._warningRecords + this._warningAnnotations; } get errors() { return this._errorRecords + this._errorAnnotations; } get noticeRecords() { return this._noticeRecords; } get warningRecords() { return this._warningRecords; } get errorRecords() { return this._errorRecords; } get noticeAnnotations() { return this._noticeAnnotations; } get warningAnnotations() { return this._warningAnnotations; } get errorAnnotations() { return this._errorAnnotations; } get records() { return this._noticeRecords + this._warningRecords + this._errorRecords; } get annotations() { return this._noticeAnnotations + this._warningAnnotations + this._errorAnnotations; } get empty() { return !(this.records + this.annotations); } annotate(annotationLevel, file, note) { if (note) { const level = annotationLevel; this._annotations.set(file, { level, note }); switch (level) { case NOTICE: this._noticeRecords++; break; case WARN: this._warningRecords++; break; case ERROR: this._errorRecords++; break; } } return this; } add(...records) { for (const record of records) { /* Normalize the basic entries in this message */ let messages = Array.isArray(record.message) ? [...record.message.map((msg) => msg.split('\n')).flat(1)] : record.message.split('\n'); messages = messages.filter((message) => !!message); if (!messages.length) { const options = { taskName: this._task, level: ERROR }; this._emitter(options, ['No message for report record']); throw BuildFailure.fail(); } const level = record.level; const file = record.file; const source = record.source || undefined; const tags = record.tags ? Array.isArray(record.tags) ? [...record.tags] : [record.tags] : []; switch (level) { case NOTICE: this._noticeRecords++; break; case WARN: this._warningRecords++; break; case ERROR: this._errorRecords++; break; } /* Line, column and characters are a bit more complicated */ let line = 0; let column = 0; let length = 1; if (file && record.line) { line = record.line; if (record.column) { column = record.column; if (record.length) { length = record.length; if (length < 0) { length = Number.MAX_SAFE_INTEGER; } } } } /* Remember our source code, line by line */ if ((file && source) && (!this._sources.has(file))) { this._sources.set(file, source.split('\n')); } /* Remember this normalized report */ let reports = this._records.get(file || nul); if (!reports) this._records.set(file || nul, reports = new Set()); reports.add({ level, messages, tags, line, column, length: length }); } /* All done */ return this; } async loadSources() { // Read files in parallel const promises = []; // Iterate through all the files having records for (const file of this._records.keys()) { if (!file) continue; if (file === nul) continue; if (this._sources.has(file)) continue; promises.push(readFile(file, 'utf-8') .then((source) => source.split('\n')) .then((lines) => this._sources.set(file, lines))); } // Await _all_ promise, ignore errors await Promise.allSettled(promises); } done(showSources) { if (showSources == null) showSources = _showSources; if (!this.empty) this._emit(showSources); if (this.errors) throw BuildFailure.fail(); } _emit(showSources) { /* Counters for all we need to print nicely */ let fPad = 0; let aPad = 0; let mPad = 0; let lPad = 0; let cPad = 0; /* Skip report all together if empty! */ if ((this._annotations.size === 0) && (this._records.size === 0)) return; /* This is GIANT: sort and convert our data for easy reporting */ const entries = [...this._annotations.keys(), ...this._records.keys()] // dedupe .filter((file, i, a) => a.indexOf(file) === i) // dedupe // sort ("null" files first - remember, "undefined" never gets sorted) .sort((a, b) => { return ((a || '') < (b || '')) ? -1 : ((a || '') > (b || '')) ? 1 : 0; }) // map to a [ file, record[], annotation? ] .map((file) => { // Get our annotation for the file const ann = file && file !== nul && this._annotations.get(file); // Get the records (or an empty record array) const records = [...(this._records.get(file) || [])] // Sort records by line / column .sort(({ line: al, column: ac }, { line: bl, column: bc }) => ((al || Number.MAX_SAFE_INTEGER) - (bl || Number.MAX_SAFE_INTEGER)) || ((ac || Number.MAX_SAFE_INTEGER) - (bc || Number.MAX_SAFE_INTEGER))) // Update our record padding length .map((record) => { if (record.line && (record.line > lPad)) lPad = record.line; if (record.column && (record.column > cPad)) cPad = record.column; for (const message of record.messages) { if (message.length > mPad) mPad = message.length; } return record; }); // Update our file and annotation padding lengths if (file && (file.length > fPad)) fPad = file.length; if (ann && (ann.note.length > aPad)) aPad = ann.note.length; // Return our entry return { file, records, annotation: ann }; }); /* Adjust paddings... */ mPad = mPad <= 100 ? mPad : 0; // limit length of padding for breakaway lines lPad = lPad.toString().length; cPad = cPad.toString().length; /* Basic emit options */ const options = { taskName: this._task, level: NOTICE }; this._emitter(options, ['']); this._emitter(options, [$und($wht(this._title))]); /* Iterate through all our [file,reports] tuple */ for (let f = 0; f < entries.length; f++) { const { file, records, annotation } = entries[f]; const source = file && file != nul && this._sources.get(file); if ((f === 0) || entries[f - 1]?.records.length) { this._emitter(options, ['']); } if (file && file !== nul && annotation) { const { level, note } = annotation; const $col = level === NOTICE ? $blu : level === WARN ? $ylw : $red; const ann = `${$gry('[')}${$col(note)}${$gry(']')}`; const pad = ''.padStart((fPad + aPad) - (file.length + note.length)); this._emitter({ ...options, level }, [$wht($und(file)), pad, ann]); } else if (file !== nul) { this._emitter(options, [$wht($und(file))]); } else if (f > 0) { this._emitter(options, ['']); // white line for the last } /* Now get each message and do our magic */ for (let r = 0; r < records.length; r++) { const { level, messages, tags, line, column, length = 1 } = records[r]; /* Prefix includes line and column */ let pfx; if (file && line) { if (column) { pfx = ` ${line.toString().padStart(lPad)}:${column.toString().padEnd(cPad)} `; } else { pfx = ` ${line.toString().padStart(lPad)}:${'-'.padEnd(cPad)} `; } } else if (file != nul) { pfx = ` ${'-'.padStart(lPad)}:${'-'.padEnd(cPad)} `; } else { pfx = ' ~ '; } const prefix = ''.padStart(pfx.length + 1); /* Nice tags */ const tag = tags.length == 0 ? '' : `${$gry('[')}${tags.map((tag) => $cyn(tag)).join($gry('|'))}${$gry(']')}`; /* Print out our messages, one by one */ if (messages.length === 1) { this._emitter({ ...options, level }, [$gry(pfx), messages[0].padEnd(mPad), tag]); } else { for (let m = 0; m < messages.length; m++) { if (!m) { // first line this._emitter({ ...options, level }, [$gry(pfx), messages[m]]); } else if (m === messages.length - 1) { // last line this._emitter({ ...options, level, prefix }, [messages[m].padEnd(mPad), tag]); } else { // in between lines this._emitter({ ...options, level, prefix }, [messages[m]]); } } } /* See if we have to / can print out the source */ if (showSources && source && source[line - 1]) { if (column) { const $col = level === NOTICE ? $blu : level === WARN ? $ylw : $red; const offset = column - 1; const text = source[line - 1] || ''; const head = $gry(text.substring(0, offset)); const body = $und($col(text.substring(offset, offset + length))); const tail = $gry(text.substring(offset + length)); this._emitter({ ...options, level, prefix }, [$gry(`| ${head}${body}${tail}`)]); } else { this._emitter({ ...options, level, prefix }, [$gry(`| ${source[line - 1]}`)]); } } } } /* Our totals (if any) */ this._emitter(options, ['']); const status = ['Found']; if (this.errors) { status.push($plur(this.errors, 'error', 'errors')); } if (this.warnings) { if (this.errors) status.push('and'); status.push($plur(this.warnings, 'warning', 'warnings')); } if (this.errors || this.warnings) { this._emitter(options, status); this._emitter(options, ['']); } /* Annotate in GitHub */ if (_githubAnnotations) { for (const entry of entries) { const file = entry.file === nul ? undefined : entry.file; for (const report of entry.records) { const type = report.level === logLevels.ERROR ? 'error' : report.level === logLevels.WARN ? 'warning' : null; if (!type) continue; const title = `${this._title} (task "${this._task}")`; const col = report.column || undefined; const line = report.line || undefined; const endColumn = report.column ? report.length >= Number.MAX_SAFE_INTEGER ? undefined : ((report.column + report.length) || undefined) : undefined; const message = report.messages.join('\n'); githubAnnotation({ type, title, file, col, line, endColumn }, message); } } } } } //# sourceMappingURL=report.js.map