// ***************************************************************************** // Copyright (C) 2019 Ericsson and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { EOL } from '@theia/core/lib/common/os'; import { Diagnostic, DiagnosticSeverity, Range } from '@theia/core/shared/vscode-languageserver-protocol'; import { FileLocationKind, ProblemMatcher, ProblemPattern, ProblemMatch, ProblemMatchData, ProblemLocationKind } from '../common/problem-matcher-protocol'; import URI from '@theia/core/lib/common/uri'; import { Severity } from '@theia/core/lib/common/severity'; import { MAX_SAFE_INTEGER } from '@theia/core/lib/common/numbers'; import { join } from 'path'; const endOfLine: string = EOL; export interface ProblemData { kind?: ProblemLocationKind; file?: string; location?: string; line?: string; character?: string; endLine?: string; endCharacter?: string; message?: string; severity?: string; code?: string; } type ProblemDataStringKeys = keyof { [K in keyof ProblemData as string extends ProblemData[K] ? K : never]: unknown }; export abstract class AbstractLineMatcher { protected patterns: ProblemPattern[] = []; protected activePatternIndex: number = 0; protected activePattern: ProblemPattern | undefined; protected cachedProblemData: ProblemData; constructor( protected matcher: ProblemMatcher ) { if (Array.isArray(matcher.pattern)) { this.patterns = matcher.pattern; } else { this.patterns = [matcher.pattern]; } this.cachedProblemData = this.getEmptyProblemData(); if (this.patterns.slice(0, this.patternCount - 1).some(p => !!p.loop)) { console.error('Problem Matcher: Only the last pattern can loop'); } } /** * Finds the problem identified by this line matcher. * * @param line the line of text to find the problem from * @return the identified problem. If the problem is not found, `undefined` is returned. */ abstract match(line: string): ProblemMatch | undefined; /** * Number of problem patterns that the line matcher uses. */ get patternCount(): number { return this.patterns.length; } protected getEmptyProblemData(): ProblemData { // eslint-disable-next-line no-null/no-null return Object.create(null) as ProblemData; } protected fillProblemData(data: ProblemData | null, pattern: ProblemPattern, matches: RegExpExecArray): data is ProblemData { if (data) { this.fillProperty(data, 'file', pattern, matches, true); this.appendProperty(data, 'message', pattern, matches, true); this.fillProperty(data, 'code', pattern, matches, true); this.fillProperty(data, 'severity', pattern, matches, true); this.fillProperty(data, 'location', pattern, matches, true); this.fillProperty(data, 'line', pattern, matches); this.fillProperty(data, 'character', pattern, matches); this.fillProperty(data, 'endLine', pattern, matches); this.fillProperty(data, 'endCharacter', pattern, matches); return true; } return false; } private appendProperty(data: ProblemData, property: ProblemDataStringKeys, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { const patternProperty = pattern[property]; if (data[property] === undefined) { this.fillProperty(data, property, pattern, matches, trim); } else if (patternProperty !== undefined && patternProperty < matches.length) { let value = matches[patternProperty]; if (trim) { value = value.trim(); } data[property] = data[property] + endOfLine + value; } } private fillProperty(data: ProblemData, property: ProblemDataStringKeys, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void { const patternAtProperty = pattern[property]; if (data[property] === undefined && patternAtProperty !== undefined && patternAtProperty < matches.length) { let value = matches[patternAtProperty]; if (value !== undefined) { if (trim) { value = value.trim(); } data[property] = value; } } } protected getMarkerMatch(data: ProblemData): ProblemMatch | undefined { try { const location = this.getLocation(data); if (data.file && location && data.message) { const marker: Diagnostic = { severity: this.getSeverity(data), range: location, message: data.message }; if (data.code !== undefined) { marker.code = data.code; } if (this.matcher.source !== undefined) { marker.source = this.matcher.source; } return { description: this.matcher, resource: this.getResource(data.file, this.matcher), marker } as ProblemMatchData; } return { description: this.matcher }; } catch (err) { console.error(`Failed to convert problem data into match: ${JSON.stringify(data)}`); } return undefined; } private getLocation(data: ProblemData): Range | null { if (data.kind === ProblemLocationKind.File) { return this.createRange(0, 0, 0, 0); } if (data.location) { return this.parseLocationInfo(data.location); } if (!data.line) { // eslint-disable-next-line no-null/no-null return null; } const startLine = parseInt(data.line); const startColumn = data.character ? parseInt(data.character) : undefined; const endLine = data.endLine ? parseInt(data.endLine) : undefined; const endColumn = data.endCharacter ? parseInt(data.endCharacter) : undefined; return this.createRange(startLine, startColumn, endLine, endColumn); } private parseLocationInfo(value: string): Range | null { if (!value || !value.match(/(\d+|\d+,\d+|\d+,\d+,\d+,\d+)/)) { // eslint-disable-next-line no-null/no-null return null; } const parts = value.split(','); const startLine = parseInt(parts[0]); const startColumn = parts.length > 1 ? parseInt(parts[1]) : undefined; if (parts.length > 3) { return this.createRange(startLine, startColumn, parseInt(parts[2]), parseInt(parts[3])); } else { return this.createRange(startLine, startColumn, undefined, undefined); } } private createRange(startLine: number, startColumn: number | undefined, endLine: number | undefined, endColumn: number | undefined): Range { let range: Range; if (startColumn !== undefined) { if (endColumn !== undefined) { range = Range.create(startLine, startColumn, endLine || startLine, endColumn); } else { range = Range.create(startLine, startColumn, startLine, startColumn); } } else { range = Range.create(startLine, 1, startLine, MAX_SAFE_INTEGER); } // range indexes should be zero-based return Range.create( this.getZeroBasedRangeIndex(range.start.line), this.getZeroBasedRangeIndex(range.start.character), this.getZeroBasedRangeIndex(range.end.line), this.getZeroBasedRangeIndex(range.end.character) ); } private getZeroBasedRangeIndex(ind: number): number { return ind === 0 ? ind : ind - 1; } private getSeverity(data: ProblemData): DiagnosticSeverity { // eslint-disable-next-line no-null/no-null let result: Severity | null = null; if (data.severity) { const value = data.severity; if (value) { result = Severity.fromValue(value); if (result === Severity.Ignore) { if (value === 'E') { result = Severity.Error; } else if (value === 'W') { result = Severity.Warning; } else if (value === 'I') { result = Severity.Info; } else if (value.toLowerCase() === 'hint') { result = Severity.Info; } else if (value.toLowerCase() === 'note') { result = Severity.Info; } } } } // eslint-disable-next-line no-null/no-null if (result === null || result === Severity.Ignore) { result = this.matcher.severity || Severity.Error; } return Severity.toDiagnosticSeverity(result); } private getResource(filename: string, matcher: ProblemMatcher): URI { const kind = matcher.fileLocation; let fullPath: string | undefined; if (kind === FileLocationKind.Absolute) { fullPath = filename; } else if ((kind === FileLocationKind.Relative) && matcher.filePrefix) { let relativeFileName = filename.replace(/\\/g, '/'); if (relativeFileName.startsWith('./')) { relativeFileName = relativeFileName.slice(2); } fullPath = join(matcher.filePrefix, relativeFileName); } if (fullPath === undefined) { throw new Error('FileLocationKind is not actionable. Does the matcher have a filePrefix? This should never happen.'); } if (matcher.uriProvider !== undefined) { return matcher.uriProvider(fullPath); } else { return URI.fromFilePath(fullPath); } } protected resetActivePatternIndex(defaultIndex?: number): void { if (defaultIndex === undefined) { defaultIndex = 0; } this.activePatternIndex = defaultIndex; this.activePattern = this.patterns[defaultIndex]; } protected nextProblemPattern(): void { this.activePatternIndex++; if (this.activePatternIndex > this.patternCount - 1) { this.resetActivePatternIndex(); } else { this.activePattern = this.patterns[this.activePatternIndex]; } } protected doOneLineMatch(line: string): boolean { if (this.activePattern) { const regexp = new RegExp(this.activePattern.regexp); const regexMatches = regexp.exec(line); if (regexMatches) { this.cachedProblemData.kind ??= this.activePattern.kind; return this.fillProblemData(this.cachedProblemData, this.activePattern, regexMatches); } } return false; } // check if active pattern is the last pattern protected isUsingTheLastPattern(): boolean { return this.patternCount > 0 && this.activePatternIndex === this.patternCount - 1; } protected isLastPatternLoop(): boolean { return this.patternCount > 0 && !!this.patterns[this.patternCount - 1].loop; } protected resetCachedProblemData(): void { this.cachedProblemData = this.getEmptyProblemData(); } }