Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Parse log after building
  • Loading branch information
pfoerster committed Mar 23, 2019
1 parent 34696fa commit 2735dea
Show file tree
Hide file tree
Showing 15 changed files with 1,225 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -91,3 +91,6 @@ out/

# dist
dist/

# build logs
!test/logs/
2 changes: 1 addition & 1 deletion src/diagnostics/bibtexEntry.ts
Expand Up @@ -139,7 +139,7 @@ export function createDiagnostic(code: ErrorCode, range: Range): Diagnostic {
}

return {
source: 'bibtex',
source: 'BibTeX',
range,
message,
severity: DiagnosticSeverity.Error,
Expand Down
178 changes: 178 additions & 0 deletions src/diagnostics/buildLog.test.ts
@@ -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);
});
});
135 changes: 135 additions & 0 deletions src/diagnostics/buildLog.ts
@@ -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;
}
4 changes: 4 additions & 0 deletions src/diagnostics/index.ts
Expand Up @@ -6,17 +6,21 @@ import {
import { concat, FeatureContext } from '../provider';
import { BibtexEntryDiagnosticsProvider } from './bibtexEntry';
import { LatexDiagnosticsProvider } from './latex';
import { ManualDiagnosticsProvider } from './manual';
import { DiagnosticsProvider } from './provider';

class DefaultDiagnosticsProvider implements DiagnosticsProvider {
public readonly latexProvider: LatexDiagnosticsProvider;
public readonly buildProvider: ManualDiagnosticsProvider;
private readonly allProviders: DiagnosticsProvider;

constructor() {
this.latexProvider = new LatexDiagnosticsProvider();
this.buildProvider = new ManualDiagnosticsProvider();
this.allProviders = concat(
BibtexEntryDiagnosticsProvider,
this.latexProvider,
this.buildProvider,
);
}

Expand Down
13 changes: 13 additions & 0 deletions src/diagnostics/manual.ts
@@ -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()) || [];
}
}
2 changes: 1 addition & 1 deletion src/languageServer.ts
Expand Up @@ -70,7 +70,7 @@ export abstract class LanguageServer {
this.connection.onInitialize(this.initialize.bind(this));
this.connection.onInitialized(this.initialized.bind(this));
this.connection.onShutdown(this.shutdown.bind(this));
this.connection.onInitialized(this.exit.bind(this));
this.connection.onExit(this.exit.bind(this));
this.connection.onDidChangeConfiguration(
this.didChangeConfiguration.bind(this),
);
Expand Down

0 comments on commit 2735dea

Please sign in to comment.