Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds fypp linting support #591

Merged
merged 17 commits into from
Aug 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@
".fpp",
".FPP",
".pf",
".PF"
".PF",
".fypp",
".FYPP"
],
"configuration": "./language-configuration.json"
},
Expand Down Expand Up @@ -211,32 +213,96 @@
"nagfor",
"Disabled"
],
"markdownDescription": "Compiler used for linting support."
"markdownDescription": "Compiler used for linting support.",
"order": 0
},
"fortran.linter.compilerPath": {
"type": "string",
"default": "",
"markdownDescription": "Specifies the path to the linter executable.",
"order": 10
},
"fortran.linter.includePaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"markdownDescription": "Specifies folder paths to be used as include paths during linting. Can resolve glob patterns e.g. `/usr/local/include/**` and internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`"
},
"fortran.linter.compilerPath": {
"type": "string",
"default": "",
"markdownDescription": "Specifies the path to the linter executable."
"markdownDescription": "Specifies folder paths to be used as include paths during linting. Can resolve glob patterns e.g. `/usr/local/include/**` and internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`.",
"order": 20
},
"fortran.linter.extraArgs": {
"type": "array",
"items": {
"type": "string"
},
"markdownDescription": "Pass additional options to the linter compiler. Can resolve internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`"
"markdownDescription": "Pass additional options to the linter compiler. Can resolve internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`.",
"order": 30
},
"fortran.linter.modOutput": {
"type": "string",
"default": "",
"markdownDescription": "Global output directory for .mod files generated due to linting `-J<linter.modOutput>`. Can resolve internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`"
"markdownDescription": "Global output directory for .mod files generated due to linting `-J<linter.modOutput>`. Can resolve internal variables with `~`, `${workspaceFolder}`, `${env}`, `${config}`, `${file}`, `${fileDirname}`, `${fileBasenameNoExtension}`.",
"order": 40
},
"fortran.linter.fypp.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "Use `fypp` to expand preprocessor directives.",
"order": 50
},
"fortran.linter.fypp.path": {
"type": "string",
"default": "fypp",
"markdownDescription": "Path to the `fypp` executable.",
"order": 60
},
"fortran.linter.fypp.definitions": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"markdownDescription": "Preprocessor definitions passed to `fypp`.",
"order": 70
},
"fortran.linter.fypp.includes": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"markdownDescription": "Include directories passed to `fypp`.",
"order": 80
},
"fortran.linter.fypp.lineNumberingMode": {
"type": "string",
"default": "full",
"enum": [
"full",
"nocontlines"
],
"markdownDescription": "line numbering mode, `full` (default): line numbering markers generated whenever source and output lines are out of sync, `nocontlines`: line numbering markers omitted for continuation lines.",
"order": 110
},
"fortran.linter.fypp.lineMarkerFormat": {
"type": "string",
"default": "cpp",
"enum": [
"cpp",
"std",
"gfortran5"
],
"markdownDescription": "ine numbering marker format, currently std`, `cpp` and `gfortran5` are supported, where `std` emits `#line` pragmas similar to standard tools, 'cpp' produces line directives as emitted by GNU cpp, and `gfortran5` cpp line directives with a workaround for a bug introduced in GFortran 5. Default: `cpp`.",
"order": 120
},
"fortran.linter.fypp.extraArgs": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"markdownDescription": "Pass additional options to the `fypp` executable.",
"order": 130
}
}
},
Expand Down Expand Up @@ -573,11 +639,5 @@
"glob": "^8.0.3",
"vscode-languageclient": "^8.0.2",
"which": "^2.0.2"
},
"__metadata": {
"id": "64379b4d-40cd-415a-8643-b07572d4a243",
"publisherDisplayName": "The Fortran Programming Language",
"publisherId": "0fb8288f-2952-4d83-8d25-46814faecc34",
"isPreReleaseVersion": false
}
}
94 changes: 87 additions & 7 deletions src/features/linter-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import which from 'which';

import * as vscode from 'vscode';
import { Logger } from '../services/logging';
import { EXTENSION_ID, FortranDocumentSelector, resolveVariables } from '../lib/tools';
import {
EXTENSION_ID,
FortranDocumentSelector,
resolveVariables,
promptForMissingTool,
isFreeForm,
} from '../lib/tools';
import { arraysEqual } from '../lib/helper';
import { RescanLint } from './commands';
import { GlobPaths } from '../lib/glob-paths';

export class FortranLintingProvider {
constructor(private logger: Logger = new Logger()) {}
constructor(private logger: Logger = new Logger()) {
// Register the Linter provider
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('Fortran');
}

private diagnosticCollection: vscode.DiagnosticCollection;
private compiler: string;
Expand All @@ -32,9 +41,6 @@ export class FortranLintingProvider {
// Register Linter commands
subscriptions.push(vscode.commands.registerCommand(RescanLint, this.rescanLinter, this));

// Register the Linter provider
this.diagnosticCollection = vscode.languages.createDiagnosticCollection('Fortran');

vscode.workspace.onDidOpenTextDocument(this.doModernFortranLint, this, subscriptions);
vscode.workspace.onDidCloseTextDocument(
textDocument => {
Expand All @@ -55,7 +61,7 @@ export class FortranLintingProvider {
this.diagnosticCollection.dispose();
}

private doModernFortranLint(textDocument: vscode.TextDocument) {
private async doModernFortranLint(textDocument: vscode.TextDocument) {
// Only lint if a compiler is specified
const config = vscode.workspace.getConfiguration('fortran.linter');
if (config.get<string>('fortran.linter.compiler') === 'Disabled') return;
Expand Down Expand Up @@ -93,6 +99,14 @@ export class FortranLintingProvider {
env: env,
});

const fyppProcess = this.getFyppProcess(textDocument);
if (fyppProcess) {
fyppProcess.stdout.on('data', (data: Buffer) => {
childProcess.stdin.write(data.toString());
childProcess.stdin.end();
});
}

if (childProcess.pid) {
childProcess.stdout.on('data', (data: Buffer) => {
compilerOutput += data;
Expand Down Expand Up @@ -134,12 +148,19 @@ export class FortranLintingProvider {

const extensionIndex = textDocument.fileName.lastIndexOf('.');
const fileNameWithoutExtension = textDocument.fileName.substring(0, extensionIndex);
const config = vscode.workspace.getConfiguration(EXTENSION_ID);
// FIXME: currently only enabled for gfortran
const fypp: boolean = config.get('linter.fypp.enabled') && this.compiler === 'gfortran';
const fortranSource: string[] = fypp
? ['-xf95', isFreeForm(textDocument) ? '-ffree-form' : '-ffixed-form', '-']
: [textDocument.fileName];

const argList = [
...args,
...this.getIncludeParams(includePaths), // include paths
textDocument.fileName,
'-o',
`${fileNameWithoutExtension}.mod`,
...fortranSource,
];

return argList.map(arg => arg.trim()).filter(arg => arg !== '');
Expand Down Expand Up @@ -530,4 +551,63 @@ export class FortranLintingProvider {
this.logger.debug(`[lint] glob paths:`, this.pathCache.get(opt).globs);
this.logger.debug(`[lint] resolved paths:`, this.pathCache.get(opt).paths);
}

/**
* Parse a source file through the `fypp` preprocessor and return and active
* process to parse as input to the main linter.
*
* This procedure does implements all the settings interfaces with `fypp`
* and checks the system for `fypp` prompting to install it if missing.
* @param document File name to pass to `fypp`
* @returns Async spawned process containing `fypp` output
*/
private getFyppProcess(document: vscode.TextDocument): cp.ChildProcess | undefined {
const config = vscode.workspace.getConfiguration(`${EXTENSION_ID}.linter.fypp`);
if (!config.get('enabled')) return undefined;
// FIXME: currently only enabled for gfortran
if (this.compiler !== 'gfortran') {
this.logger.warn(`[lint] fypp currently only supports gfortran.`);
return undefined;
}
let fypp: string = config.get('fypp.path', 'fypp');
fypp = process.platform !== 'win32' ? fypp : `${fypp}.exe`;

// Check if the fypp is installed
if (!which.sync(fypp, { nothrow: true })) {
this.logger.warn(`[lint] fypp not detected in your system. Attempting to install now.`);
const msg = `Installing fypp through pip with --user option`;
promptForMissingTool('fypp', msg, 'Python', ['Install']);
}
const args: string[] = ['--line-numbering'];

// Include paths to fypp, different from main linters include paths
// fypp includes typically pointing to folders in a projects source tree.
// While the -I options, you pass to a compiler in order to look up mod-files,
// are typically pointing to folders in the projects build tree.
const includePaths = config.get<string[]>(`includes`);
if (includePaths.length > 0) {
args.push(...this.getIncludeParams(this.getGlobPathsFromSettings(`linter.fypp.includes`)));
}

// Set the output to Fixed Format if the source is Fixed
if (!isFreeForm(document)) args.push('--fixed-format');

const fypp_defs: { [name: string]: string } = config.get('definitions');
if (Object.keys(fypp_defs).length > 0) {
// Preprocessor definitions, merge with pp_defs from fortls?
Object.entries(fypp_defs).forEach(([key, val]) => {
if (val) args.push(`-D${key}=${val}`);
else args.push(`-D${key}`);
});
}
args.push(`--line-numbering-mode=${config.get<string>('lineNumberingMode', 'full')}`);
args.push(`--line-marker-format=${config.get<string>('lineMarkerFormat', 'cpp')}`);
args.push(...`${config.get<string[]>('extraArgs', [])}`);

// The file to be preprocessed
args.push(document.fileName);

const filePath = path.parse(document.fileName).dir;
return cp.spawn(fypp, args, { cwd: filePath });
}
}
4 changes: 4 additions & 0 deletions src/lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export function isFortran(document: vscode.TextDocument): boolean {
);
}

export function isFreeForm(document: vscode.TextDocument): boolean {
return document.languageId === 'FortranFreeForm';
}

//
// Taken with minimal alterations from lsp-multi-server-sample
//
Expand Down
20 changes: 12 additions & 8 deletions test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//

// The module 'assert' provides assertion methods from node
import * as assert from 'assert';
import { strictEqual } from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
Expand All @@ -13,14 +13,18 @@ import * as path from 'path';
import { FortranDocumentSymbolProvider } from '../src/features/document-symbol-provider';

// Defines a Mocha test suite to group tests of similar kind together
suite('Extension Tests', () => {
test('symbol provider works as expected', async () => {
const filePath = path.resolve(__dirname, '../../test/fortran/sample.f90');
const openPath = vscode.Uri.file(filePath);
const doc = await vscode.workspace.openTextDocument(openPath);
vscode.window.showTextDocument(doc);
suite('Extension Integration Tests', async () => {
const filePath = path.resolve(__dirname, '../../test/fortran/sample.f90');
const openPath = vscode.Uri.file(filePath);
const doc = await vscode.workspace.openTextDocument(openPath);

suiteSetup(async () => {
await vscode.window.showTextDocument(doc);
});

test('Built-in symbol provider works as expected', async () => {
const symbolProvider = new FortranDocumentSymbolProvider();
const symbols = await symbolProvider.provideDocumentSymbols(doc, null);
assert.strictEqual(symbols.length, 1);
strictEqual(symbols.length, 1);
});
});
12 changes: 9 additions & 3 deletions test/fortran/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
{
"fortran.logging.level": "Debug",
"fortran.linter.includePaths": ["${workspaceFolder}/lint/**"],
"fortran.linter.fypp.enabled": false,
"fortran.linter.fypp.includes": ["${workspaceFolder}/fypp/**"],
"fortran.linter.fypp.definitions": {
"DEBUG": "1",
"VAL": ""
},
"fortran.fortls.disableAutoupdate": true,
"fortran.fortls.preprocessor.definitions": {
"HAVE_ZOLTAN": "",
"PetscInt": "integer(kind=selected_int_kind(5))"
},
"fortran.linter.includePaths": ["${workspaceFolder}/lint/**"],
"fortran.fortls.preprocessor.directories": ["include", "val"],
"fortran.fortls.preprocessor.suffixes": [".fypp", ".f90"],
"fortran.fortls.suffixes": [".FFF", ".F90-2018"],
"fortran.fortls.directories": ["./**"],
"fortran.fortls.excludeSuffixes": [".snap"],
"fortran.fortls.excludeDirectories": [".vscode/"],
"fortran.fortls.notifyInit": true,
"fortran.logging.level": "Debug"
"fortran.fortls.notifyInit": true
}
26 changes: 26 additions & 0 deletions test/fortran/fypp/demo.fypp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#:set data_names = [ 'eg1', 'eg2' ]
module min_eg
implicit none
private

#:for dname in data_names

public :: eg_${dname}$_t
type :: eg_${dname}$_t
contains
procedure :: do_something => do_something_${dname}$
end type eg_${dname}$_t

#:endfor

contains

#:for dname in data_names
subroutine do_something_${dname}$ (this)
implicit none
class(eg_${dname}$_t) :: this
print*, "this subroutine has no syntax highlighting with either extension"
end subroutine do_something_${dname}$
#:endfor

end module min_eg
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Loading