Skip to content

Commit befb9a8

Browse files
authored
Merge pull request #625 from fortran-lang/gnikit/issue523
2 parents 78dbd7d + 03f238c commit befb9a8

File tree

5 files changed

+249
-1
lines changed

5 files changed

+249
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
99

1010
### Added
1111

12+
- Added support for enhanced `gfotran` v11+ diagnostics using `-fdiagnostics-plain-output`
13+
([#523](https://github.com/fortran-lang/vscode-fortran-support/issues/523))
1214
- Added language icons for Free and Fixed form Fortran
1315
([#612](https://github.com/fortran-lang/vscode-fortran-support/issues/612))
1416
- Added capability for linter options to update automatically when settings change

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@
616616
"@types/glob": "^7.2.0",
617617
"@types/mocha": "^9.1.0",
618618
"@types/node": "^16.11.39",
619+
"@types/semver": "^7.3.12",
619620
"@types/vscode": "^1.63.0",
620621
"@types/which": "^2.0.1",
621622
"@typescript-eslint/eslint-plugin": "^5.33.1",
@@ -645,6 +646,7 @@
645646
"dependencies": {
646647
"fast-glob": "^3.2.11",
647648
"glob": "^8.0.3",
649+
"semver": "^7.3.7",
648650
"vscode-languageclient": "^8.0.2",
649651
"which": "^2.0.2"
650652
}

src/features/linter-provider.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import * as path from 'path';
44
import * as cp from 'child_process';
55
import which from 'which';
6-
6+
import * as semver from 'semver';
77
import * as vscode from 'vscode';
8+
89
import { Logger } from '../services/logging';
910
import {
1011
EXTENSION_ID,
@@ -19,10 +20,13 @@ import { RescanLint } from './commands';
1920
import { GlobPaths } from '../lib/glob-paths';
2021

2122
export class LinterSettings {
23+
private _modernGNU: boolean;
24+
private _version: string;
2225
private config: vscode.WorkspaceConfiguration;
2326

2427
constructor(private logger: Logger = new Logger()) {
2528
this.config = vscode.workspace.getConfiguration(EXTENSION_ID);
29+
this.GNUVersion(this.compiler); // populates version & modernGNU
2630
}
2731
public update(event: vscode.ConfigurationChangeEvent) {
2832
console.log('update settings');
@@ -50,6 +54,53 @@ export class LinterSettings {
5054
public get modOutput(): string {
5155
return this.config.get<string>('linter.modOutput');
5256
}
57+
58+
// END OF API SETTINGS
59+
60+
/**
61+
* Returns the version of the compiler and populates the internal variables
62+
* `modernGNU` and `version`.
63+
* @note Only supports `gfortran`
64+
*/
65+
private GNUVersion(compiler: string): string | undefined {
66+
// Only needed for gfortran's diagnostics flag
67+
this.modernGNU = false;
68+
if (compiler !== 'gfortran') return;
69+
const child = cp.spawnSync(compiler, ['--version']);
70+
if (child.error || child.status !== 0) {
71+
this.logger.error(`[lint] Could not spawn ${compiler} to check version.`);
72+
return;
73+
}
74+
// State the variables explicitly bc the TypeScript compiler on the CI
75+
// seemed to optimise away the stdout and regex would return null
76+
const regex = /^GNU Fortran \([\w.-]+\) (?<version>.*)$/gm;
77+
const output = child.stdout.toString();
78+
const match = regex.exec(output);
79+
const version = match ? match.groups.version : undefined;
80+
if (semver.valid(version)) {
81+
this.version = version;
82+
this.logger.info(`[lint] Found GNU Fortran version ${version}`);
83+
this.logger.debug(`[lint] Using Modern GNU Fortran diagnostics: ${this.modernGNU}`);
84+
return version;
85+
} else {
86+
this.logger.error(`[lint] invalid compiler version: ${version}`);
87+
}
88+
}
89+
90+
public get version(): string {
91+
return this._version;
92+
}
93+
private set version(version: string) {
94+
this._version = version;
95+
this.modernGNU = semver.gte(version, '11.0.0');
96+
}
97+
public get modernGNU(): boolean {
98+
return this._modernGNU;
99+
}
100+
private set modernGNU(modernGNU: boolean) {
101+
this._modernGNU = modernGNU;
102+
}
103+
53104
// FYPP options
54105

55106
public get fyppEnabled(): boolean {
@@ -365,6 +416,7 @@ export class FortranLintingProvider {
365416
const matches = [...msg.matchAll(regex)];
366417
switch (this.compiler) {
367418
case 'gfortran':
419+
if (this.settings.modernGNU) return this.linterParserGCCPlainText(matches);
368420
return this.linterParserGCC(matches);
369421

370422
case 'ifx':
@@ -419,6 +471,42 @@ export class FortranLintingProvider {
419471
return diagnostics;
420472
}
421473

474+
private linterParserGCCPlainText(matches: RegExpMatchArray[]): vscode.Diagnostic[] {
475+
const diagnostics: vscode.Diagnostic[] = [];
476+
for (const m of matches) {
477+
const g = m.groups;
478+
// m[0] is the entire match and then the captured groups follow
479+
const lineNo: number = parseInt(g['ln']);
480+
const colNo: number = parseInt(g['cn']);
481+
const msgSev: string = g['sev'];
482+
const msg: string = g['msg'];
483+
484+
const range = new vscode.Range(
485+
new vscode.Position(lineNo - 1, colNo),
486+
new vscode.Position(lineNo - 1, colNo)
487+
);
488+
489+
let severity: vscode.DiagnosticSeverity;
490+
switch (msgSev.toLowerCase()) {
491+
case 'error':
492+
case 'fatal error':
493+
severity = vscode.DiagnosticSeverity.Error;
494+
break;
495+
case 'warning':
496+
severity = vscode.DiagnosticSeverity.Warning;
497+
break;
498+
case 'info': // gfortran does not produce info AFAIK
499+
severity = vscode.DiagnosticSeverity.Information;
500+
break;
501+
default:
502+
severity = vscode.DiagnosticSeverity.Error;
503+
break;
504+
}
505+
diagnostics.push(new vscode.Diagnostic(range, msg, severity));
506+
}
507+
return diagnostics;
508+
}
509+
422510
private linterParserIntel(matches: RegExpMatchArray[]): vscode.Diagnostic[] {
423511
const diagnostics: vscode.Diagnostic[] = [];
424512
for (const m of matches) {
@@ -529,6 +617,16 @@ export class FortranLintingProvider {
529617
-------------------------------------------------------------------------
530618
*/
531619
case 'gfortran':
620+
/**
621+
-----------------------------------------------------------------------
622+
COMPILER: MESSAGE ANATOMY:
623+
file:line:column: Severity: msg
624+
-----------------------------------------------------------------------
625+
see https://regex101.com/r/73TZQn/1
626+
*/
627+
if (this.settings.modernGNU) {
628+
return /(?<fname>(?:\w:\\)?.*):(?<ln>\d+):(?<cn>\d+): (?<sev>Error|Warning|Fatal Error): (?<msg>.*)/g;
629+
}
532630
// see https://regex101.com/r/hZtk3f/1
533631
return /(?:^(?<fname>(?:\w:\\)?.*):(?<ln>\d+):(?<cn>\d+):(?:\s+.*\s+.*?\s+)(?<sev1>Error|Warning|Fatal Error):\s(?<msg1>.*)$)|(?:^(?<bin>\w+):\s*(?<sev2>\w+\s*\w*):\s*(?<msg2>.*)$)/gm;
534632

@@ -574,6 +672,9 @@ export class FortranLintingProvider {
574672
switch (compiler) {
575673
case 'flang':
576674
case 'gfortran':
675+
if (this.settings.modernGNU) {
676+
return ['-fsyntax-only', '-cpp', '-fdiagnostics-plain-output'];
677+
}
577678
return ['-fsyntax-only', '-cpp', '-fdiagnostics-show-option'];
578679

579680
// ifort theoretically supports fsyntax-only too but I had trouble

test/linter-provider.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ suite('fypp Linter integration', () => {
122122
suite('GNU (gfortran) lint single', () => {
123123
const linter = new FortranLintingProvider();
124124
linter['compiler'] = 'gfortran';
125+
linter['settings']['modernGNU'] = false;
125126
const msg = `
126127
C:\\Some\\random\\path\\sample.f90:4:18:
127128
@@ -164,6 +165,7 @@ Error: Missing actual argument for argument ‘a’ at (1)
164165
suite('GNU (gfortran) lint multiple', () => {
165166
const linter = new FortranLintingProvider();
166167
linter['compiler'] = 'gfortran';
168+
linter['settings']['modernGNU'] = false;
167169
const msg = `
168170
/fetch/main/FETCH.F90:1629:24:
169171
@@ -236,6 +238,7 @@ f951: some warnings being treated as errors
236238
suite('GNU (gfortran) lint preprocessor', () => {
237239
const linter = new FortranLintingProvider();
238240
linter['compiler'] = 'gfortran';
241+
linter['settings']['modernGNU'] = false;
239242
const msg = `
240243
gfortran: fatal error: cannot execute '/usr/lib/gcc/x86_64-linux-gnu/9/f951': execv: Argument list too long\ncompilation terminated.
241244
`;
@@ -277,6 +280,7 @@ gfortran: fatal error: cannot execute '/usr/lib/gcc/x86_64-linux-gnu/9/f951': ex
277280
suite('GNU (gfortran) lint preprocessor multiple', () => {
278281
const linter = new FortranLintingProvider();
279282
linter['compiler'] = 'gfortran';
283+
linter['settings']['modernGNU'] = false;
280284
const msg = `
281285
f951: Warning: Nonexistent include directory '/Code/TypeScript/vscode-fortran-support/test/fortran/include' [-Wmissing-include-dirs]
282286
/Code/TypeScript/vscode-fortran-support/test/fortran/sample.f90:4:18:
@@ -341,6 +345,131 @@ Error: Missing actual argument for argument 'a' at (1)
341345
deepStrictEqual(matches, ref);
342346
});
343347
});
348+
suite('GNU (gfortran v11+) lint single plain output', () => {
349+
const linter = new FortranLintingProvider();
350+
linter['compiler'] = 'gfortran';
351+
linter['settings']['modernGNU'] = true;
352+
const msg = `err-mod.f90:3:17: Error: (1)`;
353+
suite('REGEX matches', () => {
354+
const regex = linter['getCompilerREGEX'](linter['compiler']);
355+
const matches = [...msg.matchAll(regex)];
356+
const g = matches[0].groups;
357+
test('REGEX: filename', () => {
358+
strictEqual(g?.['fname'], 'err-mod.f90');
359+
});
360+
test('REGEX: line number', () => {
361+
strictEqual(g?.['ln'], '3');
362+
});
363+
test('REGEX: column number', () => {
364+
strictEqual(g?.['cn'], '17');
365+
});
366+
test('REGEX: severity <sev>', () => {
367+
strictEqual(g?.['sev'], 'Error');
368+
});
369+
test('REGEX: message <msg>', () => {
370+
strictEqual(g?.['msg'], '(1)');
371+
});
372+
});
373+
test('Diagnostics Array', () => {
374+
const matches = linter['parseLinterOutput'](msg);
375+
const ref = [
376+
new Diagnostic(
377+
new Range(new Position(2, 17), new Position(2, 17)),
378+
'(1)',
379+
DiagnosticSeverity.Error
380+
),
381+
];
382+
deepStrictEqual(matches, ref);
383+
});
384+
});
385+
suite('GNU (gfortran v11+) lint multiple plain output', () => {
386+
const linter = new FortranLintingProvider();
387+
linter['compiler'] = 'gfortran';
388+
linter['settings']['modernGNU'] = true;
389+
const msg = `
390+
err-mod.f90:3:17: Error: (1)
391+
err-mod.f90:2:11: Error: IMPLICIT NONE statement at (1) cannot follow PRIVATE statement at (2)
392+
err-mod.f90:10:22: Error: Missing actual argument for argument ‘arg1’ at (1)`;
393+
suite('REGEX matches', () => {
394+
const regex = linter['getCompilerREGEX'](linter['compiler']);
395+
const matches = [...msg.matchAll(regex)];
396+
const g = matches[0].groups;
397+
test('REGEX: filename', () => {
398+
strictEqual(g?.['fname'], 'err-mod.f90');
399+
});
400+
test('REGEX: line number', () => {
401+
strictEqual(g?.['ln'], '3');
402+
});
403+
test('REGEX: column number', () => {
404+
strictEqual(g?.['cn'], '17');
405+
});
406+
test('REGEX: severity <sev>', () => {
407+
strictEqual(g?.['sev'], 'Error');
408+
});
409+
test('REGEX: message <msg>', () => {
410+
strictEqual(g?.['msg'], '(1)');
411+
});
412+
413+
const g2 = matches[1].groups;
414+
test('REGEX: filename', () => {
415+
strictEqual(g2?.['fname'], 'err-mod.f90');
416+
});
417+
test('REGEX: line number', () => {
418+
strictEqual(g2?.['ln'], '2');
419+
});
420+
test('REGEX: column number', () => {
421+
strictEqual(g2?.['cn'], '11');
422+
});
423+
test('REGEX: severity <sev>', () => {
424+
strictEqual(g2?.['sev'], 'Error');
425+
});
426+
test('REGEX: message <msg>', () => {
427+
strictEqual(
428+
g2?.['msg'],
429+
'IMPLICIT NONE statement at (1) cannot follow PRIVATE statement at (2)'
430+
);
431+
});
432+
433+
const g3 = matches[2].groups;
434+
test('REGEX: filename', () => {
435+
strictEqual(g3?.['fname'], 'err-mod.f90');
436+
});
437+
test('REGEX: line number', () => {
438+
strictEqual(g3?.['ln'], '10');
439+
});
440+
test('REGEX: column number', () => {
441+
strictEqual(g3?.['cn'], '22');
442+
});
443+
test('REGEX: severity <sev>', () => {
444+
strictEqual(g3?.['sev'], 'Error');
445+
});
446+
test('REGEX: message <msg>', () => {
447+
strictEqual(g3?.['msg'], 'Missing actual argument for argument ‘arg1’ at (1)');
448+
});
449+
});
450+
451+
test('Diagnostics Array', () => {
452+
const matches = linter['parseLinterOutput'](msg);
453+
const ref = [
454+
new Diagnostic(
455+
new Range(new Position(2, 17), new Position(2, 17)),
456+
'(1)',
457+
DiagnosticSeverity.Error
458+
),
459+
new Diagnostic(
460+
new Range(new Position(1, 11), new Position(1, 11)),
461+
'IMPLICIT NONE statement at (1) cannot follow PRIVATE statement at (2)',
462+
DiagnosticSeverity.Error
463+
),
464+
new Diagnostic(
465+
new Range(new Position(9, 22), new Position(9, 22)),
466+
'Missing actual argument for argument ‘arg1’ at (1)',
467+
DiagnosticSeverity.Error
468+
),
469+
];
470+
deepStrictEqual(matches, ref);
471+
});
472+
});
344473

345474
// -----------------------------------------------------------------------------
346475

0 commit comments

Comments
 (0)