Skip to content

Commit 04bda89

Browse files
authored
Merge pull request #611 from fortran-lang/feature/linter-cache
Feature/linter cache
2 parents 501ffe3 + 9d49cfd commit 04bda89

File tree

13 files changed

+169
-59
lines changed

13 files changed

+169
-59
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616

1717
### Changed
1818

19+
- Changed how caching is performed in the linter; generalised code and improved
20+
performance of the cache
21+
([#611](https://github.com/fortran-lang/vscode-fortran-support/pull/611))
1922
- Changed packaging download to use `vscode.Tasks`
2023
([#608](https://github.com/fortran-lang/vscode-fortran-support/issues/608))
2124
- Renamed the default repository branch from `master` to `main`

Diff for: src/features/linter-provider.ts

+39-58
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,18 @@ import which from 'which';
66

77
import * as vscode from 'vscode';
88
import { Logger } from '../services/logging';
9-
import { FortranDocumentSelector, resolveVariables } from '../lib/tools';
10-
import * as fg from 'fast-glob';
11-
import { glob } from 'glob';
9+
import { EXTENSION_ID, FortranDocumentSelector, resolveVariables } from '../lib/tools';
1210
import { arraysEqual } from '../lib/helper';
1311
import { RescanLint } from './commands';
12+
import { GlobPaths } from '../lib/glob-paths';
1413

1514
export class FortranLintingProvider {
1615
constructor(private logger: Logger = new Logger()) {}
1716

1817
private diagnosticCollection: vscode.DiagnosticCollection;
1918
private compiler: string;
2019
private compilerPath: string;
21-
private cache = { includePaths: [''], globIncPaths: [''] };
20+
private pathCache = new Map<string, GlobPaths>();
2221

2322
public provideCodeActions(
2423
document: vscode.TextDocument,
@@ -128,7 +127,10 @@ export class FortranLintingProvider {
128127
...this.getLinterExtraArgs(this.compiler),
129128
...this.getModOutputDir(this.compiler),
130129
];
131-
const includePaths = this.getIncludePaths();
130+
const opt = 'linter.includePaths';
131+
const includePaths = this.getGlobPathsFromSettings(opt);
132+
this.logger.debug(`[lint] glob paths:`, this.pathCache.get(opt).globs);
133+
this.logger.debug(`[lint] resolved paths:`, this.pathCache.get(opt).paths);
132134

133135
const extensionIndex = textDocument.fileName.lastIndexOf('.');
134136
const fileNameWithoutExtension = textDocument.fileName.substring(0, extensionIndex);
@@ -179,61 +181,38 @@ export class FortranLintingProvider {
179181
* for the `linter.includePaths` option. The results are stored in a cache
180182
* to improve performance
181183
*
184+
* @param opt String representing a VS Code setting e.g. `linter.includePaths`
185+
*
182186
* @returns String Array of directories
183187
*/
184-
private getIncludePaths(): string[] {
185-
const config = vscode.workspace.getConfiguration('fortran');
186-
let includePaths: string[] = config.get('linter.includePaths', []);
187-
188-
// Check if we can use the cached results for the include directories no
189-
// need to evaluate the glob patterns everytime we call the linter
190-
// TODO: register command that forces re-linting
191-
// Not sure what the best approach for this one is?
192-
// Should I add a watcher to all the files under globIncPaths?
193-
// Should I add a watcher on the files under the workspace?
194-
if (arraysEqual(includePaths, this.cache['includePaths'])) {
195-
return this.cache['globIncPaths'];
196-
}
197-
198-
// Update our cache input
199-
this.cache['includePaths'] = includePaths;
200-
// Output the original include paths
201-
if (includePaths.length > 0) this.logger.debug(`[lint] include:`, includePaths);
202-
// Resolve internal variables and expand glob patterns
203-
const resIncludePaths = includePaths.map(e => resolveVariables(e));
204-
// fast-glob cannot work with Windows paths
205-
includePaths = includePaths.map(e => e.replace('/\\/g', '/'));
206-
// This needs to be after the resolvevariables since {} are used in globs
207-
try {
208-
const globIncPaths: string[] = fg.sync(resIncludePaths, {
209-
onlyDirectories: true,
210-
suppressErrors: false,
211-
});
212-
// Update values in cache
213-
this.cache['globIncPaths'] = globIncPaths;
214-
return globIncPaths;
215-
// Try to recover from fast-glob failing due to EACCES using slower more
216-
// robust glob.
217-
} catch (eacces) {
218-
this.logger.warn(`[lint] You lack read permissions for an include directory
219-
or more likely a glob match from the input 'includePaths' list. This can happen when
220-
using overly broad root level glob patters e.g. /usr/lib/** .
221-
No reason to worry. I will attempt to recover.
222-
You should consider adjusting your 'includePaths' if linting performance is slow.`);
223-
this.logger.warn(`[lint] ${eacces.message}`);
188+
private getGlobPathsFromSettings(opt: string): string[] {
189+
const config = vscode.workspace.getConfiguration(EXTENSION_ID);
190+
const globPaths: string[] = config.get(opt);
191+
// Initialise cache key and value if vscode option is not present
192+
if (!this.pathCache.has(opt)) {
193+
this.logger.debug(`[lint] Initialising cache for ${opt}`);
224194
try {
225-
const globIncPaths: string[] = [];
226-
for (const i of resIncludePaths) {
227-
// use '/' to match only directories and not files
228-
globIncPaths.push(...glob.sync(i + '/', { strict: false }));
229-
}
230-
this.cache['globIncPaths'] = globIncPaths;
231-
return globIncPaths;
232-
// if we failed again then our includes are somehow wrong. Abort
195+
this.pathCache.set(opt, new GlobPaths(globPaths));
233196
} catch (error) {
234-
this.logger.error(`[lint] Include path glob resolution failed to recover: ${error}`);
197+
const msg = `[lint] Error initialising cache for ${opt}`;
198+
this.logger.error(msg, error);
199+
vscode.window.showErrorMessage(`${msg}: ${error}`);
235200
}
236201
}
202+
// Check if cache is valid, and if so return cached value
203+
if (arraysEqual(globPaths, this.pathCache.get(opt).globs)) {
204+
return this.pathCache.get(opt).paths;
205+
}
206+
// Update cache and return new values
207+
try {
208+
this.pathCache.get(opt).update(globPaths);
209+
} catch (error) {
210+
const msg = `[lint] Error initialising cache for ${opt}`;
211+
this.logger.error(msg, error);
212+
vscode.window.showErrorMessage(`${msg}: ${error}`);
213+
}
214+
this.logger.debug(`[lint] ${opt} changed, updating cache`);
215+
return this.pathCache.get(opt).paths;
237216
}
238217

239218
/**
@@ -543,10 +522,12 @@ export class FortranLintingProvider {
543522
* Regenerate the cache for the include files paths of the linter
544523
*/
545524
private rescanLinter() {
525+
const opt = 'linter.includePaths';
546526
this.logger.debug(`[lint] Resetting linter include paths cache`);
547-
this.logger.debug(`[lint] Current linter include paths cache:`, this.cache['includePaths']);
548-
this.cache['includePaths'] = [];
549-
this.getIncludePaths();
550-
this.logger.debug(`[lint] New linter include paths cache:`, this.cache['includePaths']);
527+
this.logger.debug(`[lint] Current linter include paths cache:`, this.pathCache.get(opt).globs);
528+
this.pathCache.set(opt, new GlobPaths());
529+
this.getGlobPathsFromSettings(opt);
530+
this.logger.debug(`[lint] glob paths:`, this.pathCache.get(opt).globs);
531+
this.logger.debug(`[lint] resolved paths:`, this.pathCache.get(opt).paths);
551532
}
552533
}

Diff for: src/lib/glob-paths.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
import * as fg from 'fast-glob';
4+
import { glob } from 'glob';
5+
6+
import { resolveVariables } from './tools';
7+
8+
/**
9+
* A class meant to resolve glob patterns to absolute paths.
10+
* Glob resolution can be expensive, so this class can be used for caching
11+
*/
12+
export class GlobPaths {
13+
public globs: string[] = [];
14+
public paths: string[] = [];
15+
constructor(globs: string[] = []) {
16+
this.update(globs);
17+
}
18+
19+
public update(globs: string[]): void {
20+
this.globs = globs;
21+
this.paths = this.globResolution(globs);
22+
}
23+
24+
public add(globs: string[]): void {
25+
this.globs = this.globs.concat(globs);
26+
this.paths = this.paths.concat(this.globResolution(globs));
27+
}
28+
29+
/**
30+
*
31+
* @param globPaths A list of glob paths to resolve
32+
* @returns A list of resolved paths
33+
*/
34+
private globResolution(globPaths: string[]): string[] {
35+
if (globPaths.length === 0) return [];
36+
// Resolve internal variables and expand glob patterns
37+
const globPathsVars = globPaths.map(e => resolveVariables(e));
38+
// fast-glob cannot work with Windows paths
39+
globPaths = globPaths.map(e => e.replace('/\\/g', '/'));
40+
// This needs to be after the resolvevariables since {} are used in globs
41+
try {
42+
const globIncPaths: string[] = fg.sync(globPathsVars, {
43+
onlyDirectories: true,
44+
suppressErrors: false,
45+
});
46+
return globIncPaths;
47+
// Try to recover from fast-glob failing due to EACCES using slower more
48+
// robust glob.
49+
} catch (eacces) {
50+
try {
51+
const globIncPaths: string[] = [];
52+
for (const i of globPathsVars) {
53+
// use '/' to match only directories and not files
54+
globIncPaths.push(...glob.sync(i + '/', { strict: false }));
55+
}
56+
return globIncPaths;
57+
// if we failed again then our globs are somehow wrong. Abort
58+
} catch (error) {
59+
throw new TypeError(`Invalid glob syntax, error: ${error}`);
60+
}
61+
}
62+
}
63+
}

Diff for: test/fortran/.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"HAVE_ZOLTAN": "",
55
"PetscInt": "integer(kind=selected_int_kind(5))"
66
},
7+
"fortran.linter.includePaths": ["${workspaceFolder}/lint/**"],
78
"fortran.fortls.preprocessor.directories": ["include", "val"],
89
"fortran.fortls.preprocessor.suffixes": [".fypp", ".f90"],
910
"fortran.fortls.suffixes": [".FFF", ".F90-2018"],

Diff for: test/fortran/lint/lvl1-a/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-a/lvl2-a/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-a/lvl2-b/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-a/lvl2-c/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-b/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-b/lvl2-a/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-b/lvl2-b/.gitkeep

Whitespace-only changes.

Diff for: test/fortran/lint/lvl1-b/lvl2-c/.gitkeep

Whitespace-only changes.

Diff for: test/linter-provider.test.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,70 @@
11
import * as path from 'path';
22
import { strictEqual, deepStrictEqual } from 'assert';
3-
import { Diagnostic, DiagnosticSeverity, Range, Position, window, workspace, Uri } from 'vscode';
3+
import {
4+
Diagnostic,
5+
DiagnosticSeverity,
6+
Range,
7+
Position,
8+
window,
9+
workspace,
10+
Uri,
11+
TextDocument,
12+
} from 'vscode';
13+
import * as fg from 'fast-glob';
14+
import * as cp from 'child_process';
15+
416
import { FortranLintingProvider } from '../src/features/linter-provider';
517
import { delay } from '../src/lib/helper';
18+
import { EXTENSION_ID } from '../src/lib/tools';
19+
20+
suite('Linter integration', () => {
21+
let doc: TextDocument;
22+
const linter = new FortranLintingProvider();
23+
const fileUri = Uri.file(path.resolve(__dirname, '../../test/fortran/lint/test1.f90'));
24+
const root = path.resolve(__dirname, '../../test/fortran/');
25+
const config = workspace.getConfiguration(EXTENSION_ID);
26+
const oldVals = config.get<string[]>('linter.includePaths');
27+
28+
suiteSetup(async function (): Promise<void> {
29+
doc = await workspace.openTextDocument(fileUri);
30+
await window.showTextDocument(doc);
31+
});
32+
33+
test('Include path globs & internal variable resolution', async () => {
34+
const paths = linter['getGlobPathsFromSettings']('linter.includePaths');
35+
const refs: string[] = fg.sync(path.dirname(fileUri.path) + '/**', { onlyDirectories: true });
36+
deepStrictEqual(paths, refs);
37+
});
38+
39+
test('Path cache contains expected values', async () => {
40+
let refs: string[] = ['${workspaceFolder}/lint/**'];
41+
deepStrictEqual(linter['pathCache'].get('linter.includePaths')?.globs, refs);
42+
refs = fg.sync(path.join(root, 'lint') + '/**', { onlyDirectories: true });
43+
deepStrictEqual(linter['pathCache'].get('linter.includePaths')?.paths, refs);
44+
});
45+
46+
test('Update paths using cache', async () => {
47+
const refs: string[] = fg.sync([path.join(root, 'lint') + '/**', path.join(root, 'debug')], {
48+
onlyDirectories: true,
49+
});
50+
await config.update(
51+
'linter.includePaths',
52+
['${workspaceFolder}/lint/**', path.join(root, 'debug')],
53+
false
54+
);
55+
const paths = linter['getGlobPathsFromSettings']('linter.includePaths');
56+
deepStrictEqual(paths, refs);
57+
});
58+
59+
suiteTeardown(async function (): Promise<void> {
60+
await config.update('linter.includePaths', oldVals, false);
61+
// We have to reset the formatting of the files, for some reason updating
62+
// the config breaks any the formatting style.
63+
cp.spawn('npm', ['run', 'format'], { cwd: path.resolve(root, '../../') });
64+
});
65+
});
66+
67+
// -----------------------------------------------------------------------------
668

769
suite('GNU (gfortran) lint single', () => {
870
const linter = new FortranLintingProvider();

0 commit comments

Comments
 (0)