Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Parse log after building
- Loading branch information
Showing
15 changed files
with
1,225 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,3 +91,6 @@ out/ | |
|
||
# dist | ||
dist/ | ||
|
||
# build logs | ||
!test/logs/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { Uri } from '../uri'; | ||
import { BuildError, BuildErrorKind, parseLog } from './buildLog'; | ||
|
||
describe('Build Log Parser', () => { | ||
function getUri(name: string) { | ||
let file = path.join(__dirname, name); | ||
for (let i = 0; i < 26; i++) { | ||
const upperCase = String.fromCharCode('A'.charCodeAt(0) + i); | ||
const lowerCase = String.fromCharCode('a'.charCodeAt(0) + i); | ||
file = file.replace(upperCase + ':', lowerCase + ':'); | ||
} | ||
return Uri.file(file); | ||
} | ||
|
||
const parent = getUri('parent.tex'); | ||
const child = getUri('child.tex'); | ||
|
||
async function run(name: string, expected: BuildError[]) { | ||
const file = path.join(__dirname, '..', '..', 'test', 'logs', name); | ||
const text = await fs.promises.readFile(file); | ||
|
||
const actual = parseLog(parent, text.toString()); | ||
|
||
expect(actual).toHaveLength(expected.length); | ||
for (let i = 0; i < expected.length; i++) { | ||
expect(actual[i].uri.equals(expected[i].uri)).toBeTruthy(); | ||
expect(actual[i].kind).toEqual(expected[i].kind); | ||
expect(actual[i].message).toEqual(expected[i].message); | ||
expect(actual[i].line).toEqual(expected[i].line); | ||
} | ||
} | ||
|
||
it('should parse bad boxes', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: parent, | ||
message: | ||
'Overfull \\hbox (200.00162pt too wide) in paragraph at lines 8--9', | ||
kind: BuildErrorKind.Warning, | ||
line: 7, | ||
}, | ||
{ | ||
uri: parent, | ||
message: 'Overfull \\vbox (3.19998pt too high) detected at line 23', | ||
kind: BuildErrorKind.Warning, | ||
line: 22, | ||
}, | ||
]; | ||
await run('bad-box.txt', expected); | ||
}); | ||
|
||
it('should parse citation warnings', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Warning, | ||
message: "Citation `foo' on page 1 undefined on input line 6.", | ||
line: undefined, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Warning, | ||
message: 'There were undefined references.', | ||
line: undefined, | ||
}, | ||
]; | ||
await run('citation-warning.txt', expected); | ||
}); | ||
|
||
it('should find errors in related documents', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: child, | ||
kind: BuildErrorKind.Error, | ||
message: 'Undefined control sequence.', | ||
line: 0, | ||
}, | ||
]; | ||
await run('child-error.txt', expected); | ||
}); | ||
|
||
it('should parse package errors', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: | ||
"Package babel Error: Unknown option `foo'. Either you misspelled it or " + | ||
'the language definition file foo.ldf was not found.', | ||
line: 392, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: | ||
"Package babel Error: You haven't specified a language option.", | ||
line: 425, | ||
}, | ||
]; | ||
await run('package-error.txt', expected); | ||
}); | ||
|
||
it('should parse package warnings', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Warning, | ||
message: | ||
"'babel/polyglossia' detected but 'csquotes' missing. Loading 'csquotes' recommended.", | ||
line: undefined, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Warning, | ||
message: 'There were undefined references.', | ||
line: undefined, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Warning, | ||
message: | ||
'Please (re)run Biber on the file: parent and rerun LaTeX afterwards.', | ||
line: undefined, | ||
}, | ||
]; | ||
await run('package-warning.txt', expected); | ||
}); | ||
|
||
it('should parse TeX errors', async () => { | ||
const expected: BuildError[] = [ | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Undefined control sequence.', | ||
line: 6, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Missing $ inserted.', | ||
line: 7, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Undefined control sequence.', | ||
line: 8, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Missing { inserted.', | ||
line: 9, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Missing $ inserted.', | ||
line: 9, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: 'Missing } inserted.', | ||
line: 9, | ||
}, | ||
{ | ||
uri: parent, | ||
kind: BuildErrorKind.Error, | ||
message: '==> Fatal error occurred, no output PDF file produced!', | ||
line: undefined, | ||
}, | ||
]; | ||
await run('tex-error.txt', expected); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { EOL } from 'os'; | ||
import * as path from 'path'; | ||
import { Uri } from '../uri'; | ||
|
||
export enum BuildErrorKind { | ||
Error, | ||
Warning, | ||
} | ||
|
||
export interface BuildError { | ||
uri: Uri; | ||
kind: BuildErrorKind; | ||
message: string; | ||
line?: number; | ||
} | ||
|
||
export function parseLog(parent: Uri, log: string): BuildError[] { | ||
const errors: BuildError[] = []; | ||
log = prepareLog(log); | ||
|
||
const ranges = parseFileRanges(parent, log); | ||
function resolveFile(index: number) { | ||
const range = ranges.find(x => x.contains(index)); | ||
return range === undefined ? parent : range.uri || parent; | ||
} | ||
|
||
let match; | ||
const errorRegex = /^! (((.|\r|\n)*?)\r?\nl\.(\d+)|([^\r\n]*))/gm; | ||
while ((match = errorRegex.exec(log))) { | ||
const message = (match[2] || match[5]).split(/\r?\n/)[0].trim(); | ||
const line = | ||
match[4] === undefined ? undefined : parseInt(match[4], 10) - 1; | ||
|
||
errors.push({ | ||
uri: resolveFile(match.index), | ||
message, | ||
kind: BuildErrorKind.Error, | ||
line, | ||
}); | ||
} | ||
|
||
const badBoxRegex = /((Ov|Und)erfull \\[hv]box[^\r\n]*lines? (\d+)[^\r\n]*)/g; | ||
while ((match = badBoxRegex.exec(log))) { | ||
const message = match[1]; | ||
const line = parseInt(match[3], 10) - 1; | ||
errors.push({ | ||
uri: resolveFile(match.index), | ||
message, | ||
kind: BuildErrorKind.Warning, | ||
line, | ||
}); | ||
} | ||
|
||
const warningRegex = /(LaTeX|Package [a-zA-Z_\-]+) Warning: ([^\r\n]*)/g; | ||
while ((match = warningRegex.exec(log))) { | ||
const message = match[2]; | ||
errors.push({ | ||
uri: resolveFile(match.index), | ||
message, | ||
kind: BuildErrorKind.Warning, | ||
line: undefined, | ||
}); | ||
} | ||
|
||
return errors; | ||
} | ||
|
||
function prepareLog(log: string): string { | ||
const MAX_LINE_LENGTH = 79; | ||
const oldLines = log.split(/\r?\n/); | ||
const newLines: string[] = []; | ||
let index = 0; | ||
while (index < oldLines.length) { | ||
const line = oldLines[index]; | ||
const match = line.match(/^\([a-zA-Z_\-]+\)\s*(.*)$/); | ||
// Remove the package name from the following warnings: | ||
// | ||
// Package biblatex Warning: 'babel/polyglossia' detected but 'csquotes' missing. | ||
// (biblatex) Loading 'csquotes' recommended. | ||
if (match !== null) { | ||
newLines[newLines.length - 1] += ' ' + match[1]; | ||
} else if (line.endsWith('...')) { | ||
newLines.push(line.substring(0, line.length - 3)); | ||
newLines[newLines.length - 1] += oldLines[index++]; | ||
} else if (line.length === MAX_LINE_LENGTH) { | ||
newLines.push(line); | ||
newLines[newLines.length - 1] += oldLines[index++]; | ||
} else { | ||
newLines.push(line); | ||
} | ||
index++; | ||
} | ||
return newLines.join(EOL); | ||
} | ||
|
||
class FileRange { | ||
public readonly length: number; | ||
|
||
constructor( | ||
public readonly uri: Uri | undefined, | ||
public readonly start: number, | ||
public readonly end: number, | ||
) { | ||
this.length = end - start + 1; | ||
} | ||
|
||
public contains(index: number) { | ||
return index >= this.start && index <= this.end; | ||
} | ||
} | ||
|
||
function parseFileRanges(parent: Uri, log: string): FileRange[] { | ||
const ranges: FileRange[] = []; | ||
const regex = /\(([^\r\n()]+\.(tex|sty|cls))/g; | ||
let match; | ||
while ((match = regex.exec(log))) { | ||
let balance = 1; | ||
let end = match.index + 1; | ||
while (balance > 0 && end < log.length) { | ||
if (log[end] === '(') { | ||
balance++; | ||
} else if (log[end] === ')') { | ||
balance--; | ||
} | ||
end++; | ||
} | ||
|
||
const basePath = path.dirname(parent.fsPath); | ||
const fullPath = path.resolve(basePath, match[1]); | ||
const uri = fullPath.startsWith(basePath) ? Uri.file(fullPath) : undefined; | ||
ranges.push(new FileRange(uri, match.index, end)); | ||
} | ||
ranges.sort((x, y) => x.length - y.length); | ||
return ranges; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Diagnostic, TextDocumentIdentifier } from 'vscode-languageserver'; | ||
import { FeatureContext } from '../provider'; | ||
import { DiagnosticsProvider } from './provider'; | ||
|
||
export class ManualDiagnosticsProvider implements DiagnosticsProvider { | ||
public diagnosticsByUri: Map<string, Diagnostic[]> = new Map(); | ||
|
||
public async execute( | ||
context: FeatureContext<{ textDocument: TextDocumentIdentifier }>, | ||
): Promise<Diagnostic[]> { | ||
return this.diagnosticsByUri.get(context.uri.toString()) || []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.