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

Add lint scope setting #149

Closed
Closed
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
42 changes: 37 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"version": "0.2.1",
"license": "MIT",
"engines": {
"vscode": "^1.62.0"
"vscode": "^1.75.0"
},
"repository": {
"type": "git",
Expand All @@ -18,8 +18,7 @@
"Linters"
],
"activationEvents": [
"onLanguage:crystal",
"onCommand:crystal.ameba.lint"
"onLanguage:crystal"
],
"main": "./out/extension.js",
"contributes": {
Expand All @@ -43,8 +42,37 @@
{
"command": "crystal.ameba.disable",
"title": "Crystal Ameba: disable lints (workspace)"
},
{
"command": "crystal.ameba.lint-workspace",
"title": "Crystal Ameba: lint all files in workspace"
}
]
],
"configuration": {
"type": "object",
"title": "Crystal Ameba configuration",
"properties": {
"crystal-ameba.lint-scope": {
"type": "string",
"description": "Whether the linter should only care about open files or all files in the workspace.",
"default": "file",
"enum": [
"file",
"workspace"
]
},
"crystal-ameba.lint-trigger": {
"type": "string",
"description": "When the linter should be executed. Set to `none` to disable automatic linting.",
"default": "type",
"enum": [
"none",
"save",
"type"
]
}
}
}
},
"scripts": {
"vscode:prepublish": "yarn run compile",
Expand All @@ -55,9 +83,13 @@
"devDependencies": {
"@types/mocha": "^10.0.9",
"@types/node": "^22.9.0",
"@types/vscode": "^1.75.0",
"@types/semver": "^7.5.8",
"tslint": "^6.1.3",
"typescript": "^5.6.3",
"@types/vscode": "^1.95.0",
"vscode-test": "^1.6.1"
},
"dependencies": {
"semver": "^7.6.3"
}
}
228 changes: 158 additions & 70 deletions src/ameba.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exec } from 'child_process';
import { spawn } from 'child_process';
import * as path from 'path';
import { existsSync } from 'fs';
import {
Expand All @@ -10,11 +10,15 @@ import {
TextDocument,
Uri,
window,
workspace
workspace,
WorkspaceFolder
} from 'vscode';

import { AmebaOutput } from './amebaOutput';
import { AmebaConfig, getConfig } from './configuration';
import { Task, TaskQueue } from './taskQueue';
import { documentIsVirtual, noWorkspaceFolder, outputChannel } from './extension';


export class Ameba {
private diag: DiagnosticCollection;
Expand All @@ -27,85 +31,164 @@ export class Ameba {
this.config = getConfig();
}

public execute(document: TextDocument): void {
if (document.languageId !== 'crystal' || document.isUntitled || document.uri.scheme !== 'file') {
return;
public execute(document: TextDocument | WorkspaceFolder, virtual: boolean = false): void {
const isWorkspace = !('languageId' in document);
if (isWorkspace) virtual = false;

if (!isWorkspace) {
if (document.languageId !== 'crystal') return;
if (documentIsVirtual(document) && !virtual) return;
}

const dir = (workspace.getWorkspaceFolder(document.uri) ?? noWorkspaceFolder(document.uri)).uri.fsPath;

const args = [this.config.command, '--format', 'json'];

if (!isWorkspace) {
if (!virtual) {
args.push(document.fileName)
} else {
args.push('--stdin-filename', document.fileName);

// Disabling these as they're common when typing
args.push('--except', 'Lint/Formatting,Layout/TrailingBlankLines,Layout/TrailingWhitespace,Naming/Filename');
}
}

const args = [this.config.command, document.fileName, '--format', 'json'];
const dir = workspace.getWorkspaceFolder(document.uri)!.uri.fsPath;
const configFile = path.join(dir, this.config.configFileName);
if (existsSync(configFile)) args.push('--config', configFile);

const task = new Task(document.uri, token => {
const proc = exec(args.join(' '), (err, stdout, stderr) => {
if (token.isCanceled) return;
this.diag.delete(document.uri);

if (err && stderr.length) {
if ((process.platform == 'win32' && err.code === 1) || err.code === 127) {
window.showErrorMessage(
`Could not execute Ameba file${args[0] === 'ameba' ? '.' : ` at ${args[0]}`}`,
'Disable (workspace)'
).then(
disable => disable && commands.executeCommand('crystal.ameba.disable'),
_ => { }
);
} else {
window.showErrorMessage(stderr);
}
return;
}
return new Promise((resolve, reject) => {
let stdoutArr: string[] = [];
let stderrArr: string[] = [];

let results: AmebaOutput;
try {
results = JSON.parse(stdout);
} catch (err) {
console.error(`Ameba: failed parsing JSON: ${err}`);
window.showErrorMessage('Ameba: failed to parse JSON response.');
return;
outputChannel.appendLine(`$ ${args.join(' ')}`)
const proc = spawn(args[0], args.slice(1), { cwd: dir });

if (virtual && !isWorkspace) {
const documentText: string = document.getText();
proc.stdin.write(documentText)
proc.stdin.end();
}

if (!results.summary.issues_count) return;
const diagnostics: [Uri, Diagnostic[]][] = [];
token.onCancellationRequested(_ => {
proc.kill();
})

for (let source of results.sources) {
if (!source.issues.length) continue;
let parsed: Diagnostic[] = [];
proc.stdout.on('data', (data) => {
stdoutArr.push(data.toString());
})

source.issues.forEach(issue => {
let start = issue.location;
let end = issue.end_location;
if (!end.line || !end.column) {
end = start;
}
const range = new Range(
start.line - 1,
start.column - 1,
end.line - 1,
end.column
);

const diag = new Diagnostic(
range,
`[${issue.rule_name}] ${issue.message}`,
this.parseSeverity(issue.severity)
);
diag.code = {
value: "Docs",
target: Uri.parse(`https://crystaldoc.info/github/crystal-ameba/ameba/v${results.metadata.ameba_version}/Ameba/Rule/${issue.rule_name}.html`)
proc.stderr.on('data', (data) => {
stderrArr.push(data.toString());
})

proc.on('error', (err) => {
console.error('Failed to start subprocess:', err);
window.showErrorMessage(`Failed to start Ameba: ${err.message}`)
reject(err);
})

proc.on('close', (code) => {
if (token.isCancellationRequested) {
resolve();
return;
}

this.diag.delete(document.uri);

const stdout = stdoutArr.join('')
const stderr = stderrArr.join('')

if (code !== 0 && stderr.length) {
if ((process.platform == 'win32' && code === 1) || code === 127) {
window.showErrorMessage(
`Could not execute Ameba file${args[0] === 'ameba' ? '.' : ` at ${args[0]}`}`,
'Disable (workspace)'
).then(
disable => disable && commands.executeCommand('crystal.ameba.disable'),
_ => { }
);
} else {
window.showErrorMessage(stderr);
}

parsed.push(diag);
});
reject(new Error(stderr));
return;
}

diagnostics.push([document.uri, parsed]);
}
let results: AmebaOutput;

try {
results = JSON.parse(stdout);
} catch (err) {
console.error(`Ameba: failed parsing JSON: ${err}`);
outputChannel.appendLine(`[Task] Error: failed to parse JSON:\n${stdout}`)
window.showErrorMessage('Ameba: failed to parse JSON response.');
reject(err);
return;
}

if (!results.summary.issues_count) {
resolve();
return;
}

this.diag.set(diagnostics);
});
const diagnostics: [Uri, Diagnostic[]][] = [];

for (let source of results.sources) {
if (!source.issues.length) continue;
let parsed: Diagnostic[] = [];

source.issues.forEach(issue => {
let start = issue.location;
let end = issue.end_location;

if (!end.line || !end.column) {
end = start;
}

const range = new Range(
start.line - 1,
start.column - 1,
end.line - 1,
end.column
);

const diag = new Diagnostic(
range,
`[${issue.rule_name}] ${issue.message}`,
this.parseSeverity(issue.severity)
);

diag.code = {
value: "Docs",
target: Uri.parse(`https://crystaldoc.info/github/crystal-ameba/ameba/v${results.metadata.ameba_version}/Ameba/Rule/${issue.rule_name}.html`)
}

parsed.push(diag);
});

let diagnosticUri: Uri;
if (path.isAbsolute(source.path)) {
diagnosticUri = Uri.parse(source.path)
} else if (!isWorkspace && document.isUntitled) {
diagnosticUri = document.uri;
} else {
diagnosticUri = Uri.parse(path.join(dir, source.path));
}

const logPath = path.relative(dir, diagnosticUri.fsPath)
outputChannel.appendLine(`[Task] (${logPath}) Found ${parsed.length} issues`)
diagnostics.push([diagnosticUri, parsed]);
}

return () => proc.kill();
this.diag.set(diagnostics);
outputChannel.appendLine(`[Task] Done!`)
resolve();
});
})
});

this.taskQueue.enqueue(task);
Expand All @@ -124,11 +207,16 @@ export class Ameba {
}
}

public clear(document: TextDocument): void {
let uri = document.uri;
if (uri.scheme === 'file') {
this.taskQueue.cancel(uri);
this.diag.delete(uri);
public clear(document: TextDocument | null = null): void {
if (document) {
let uri = document.uri;
if (uri.scheme === 'file') {
this.taskQueue.cancel(uri);
this.diag.delete(uri);
}
} else {
this.taskQueue.clear();
this.diag.clear();
}
}
}
2 changes: 1 addition & 1 deletion src/amebaOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ export interface AmebaOutput {
metadata: AmebaMetadata;
sources: Array<AmebaFile>;
summary: AmebaSummary;
}
}
Loading
Loading