Skip to content

Commit 63aac9c

Browse files
authored
feat: add inline deno lint diagnostics (#162)
1 parent d9f74ff commit 63aac9c

File tree

8 files changed

+281
-60
lines changed

8 files changed

+281
-60
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ console.log("concat Array", M.concat([1, 2], [2, 3]));
139139

140140
- `deno.unstable` - If Deno's unstable mode is enabled. Default is `false`
141141

142+
- `deno.lint` - If inline `deno lint` diagnostics are enabled. Because this is experimental, `deno.unstable = true` is required. Default is `false`
143+
142144
We recommend that you do not set global configuration. It should be configured in `.vscode/settings.json` in the project directory:
143145

144146
```json5
@@ -164,6 +166,19 @@ This extension also provides Deno's formatting tools, settings are in `.vscode/s
164166
}
165167
```
166168

169+
This extension also provides inline `deno lint` diagnostics. You can enable this in `.vscode/settings.json`:
170+
171+
NOTE: Since `deno lint` is still an experimental feature, you need to set `deno.unstable = true` in your VS Code settings. This function may change in the future.
172+
173+
```json5
174+
// .vscode/settings.json
175+
{
176+
"deno.enable": true,
177+
"deno.unstable": true,
178+
"deno.lint": true,
179+
}
180+
```
181+
167182
## Contribute
168183

169184
Follow these steps to contribute, the community needs your strength.

client/src/extension.ts

+64-16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TextDocument,
2121
languages,
2222
env,
23+
Position,
2324
} from "vscode";
2425
import {
2526
LanguageClient,
@@ -236,7 +237,10 @@ export class Extension {
236237
}
237238
const denoDiagnostics: Diagnostic[] = [];
238239
for (const diagnostic of context.diagnostics) {
239-
if (diagnostic.source === "Deno Language Server") {
240+
if (
241+
diagnostic.source === "Deno Language Server" ||
242+
diagnostic.source === "deno_lint"
243+
) {
240244
denoDiagnostics.push(diagnostic);
241245
}
242246
}
@@ -348,29 +352,39 @@ Executable ${this.denoInfo.executablePath}`;
348352
[command: string]: (
349353
editor: TextEditor,
350354
text: string,
351-
range: Range
355+
range: Range,
356+
...args: unknown[]
352357
) => void | Promise<void>;
353358
}) {
354359
for (const command in map) {
355360
const handler = map[command];
356-
this.registerCommand(command, async (uri: string, range: Range) => {
357-
const textEditor = window.activeTextEditor;
361+
this.registerCommand(
362+
command,
363+
async (uri: string, range: Range, ...args: unknown[]) => {
364+
const textEditor = window.activeTextEditor;
358365

359-
if (!textEditor || textEditor.document.uri.toString() !== uri) {
360-
return;
361-
}
366+
if (!textEditor || textEditor.document.uri.toString() !== uri) {
367+
return;
368+
}
362369

363-
range = new Range(
364-
range.start.line,
365-
range.start.character,
366-
range.end.line,
367-
range.end.character
368-
);
370+
range = new Range(
371+
range.start.line,
372+
range.start.character,
373+
range.end.line,
374+
range.end.character
375+
);
369376

370-
const rangeText = textEditor.document.getText(range);
377+
const rangeText = textEditor.document.getText(range);
371378

372-
return await handler.call(this, textEditor, rangeText, range);
373-
});
379+
return await handler.call(
380+
this,
381+
textEditor,
382+
rangeText,
383+
range,
384+
...args
385+
);
386+
}
387+
);
374388
}
375389
}
376390
// update diagnostic for a Document
@@ -580,6 +594,40 @@ Executable ${this.denoInfo.executablePath}`;
580594

581595
this.updateDiagnostic(editor.document.uri);
582596
},
597+
_ignore_next_line_lint: async (editor, _, range, rule: unknown) => {
598+
editor.edit((edit) => {
599+
const currentLineText = editor.document.lineAt(range.start.line);
600+
const previousLineText = editor.document.lineAt(range.start.line - 1);
601+
602+
const offset =
603+
currentLineText.text.length - currentLineText.text.trim().length;
604+
605+
if (/^\s*\/\/\s+deno-lint-ignore\s*/.test(previousLineText.text)) {
606+
edit.replace(
607+
previousLineText.range,
608+
previousLineText.text + " " + rule
609+
);
610+
} else {
611+
edit.replace(
612+
previousLineText.range,
613+
previousLineText.text +
614+
"\n" +
615+
`${" ".repeat(offset)}// deno-lint-ignore ${rule}`
616+
);
617+
}
618+
});
619+
return;
620+
},
621+
_ignore_entry_file: async (editor) => {
622+
editor.edit((edit) => {
623+
const firstLineText = editor.document.lineAt(0);
624+
edit.insert(
625+
new Position(0, 0),
626+
"// deno-lint-ignore-file" + (firstLineText.text ? "\n" : "")
627+
);
628+
});
629+
return;
630+
},
583631
});
584632

585633
this.watchConfiguration(() => {

core/configuration.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ test("core / configuration / resolveFromVscode if it is a valid file", async ()
1515
enable: true,
1616
unstable: true,
1717
import_map: "./import_map.json",
18+
lint: false,
1819
} as ConfigurationField);
1920
});
2021

@@ -29,6 +30,7 @@ test("core / configuration / resolveFromVscode if valid section", async () => {
2930
enable: true,
3031
unstable: false,
3132
import_map: null,
33+
lint: false,
3234
} as ConfigurationField);
3335
});
3436

@@ -51,6 +53,7 @@ test("core / configuration / resolveFromVscode if config file is empty", async (
5153
enable: false,
5254
unstable: false,
5355
import_map: null,
56+
lint: false,
5457
} as ConfigurationField);
5558
});
5659

@@ -65,6 +68,7 @@ test("core / configuration / resolveFromVscode if field is invalid", async () =>
6568
enable: true,
6669
unstable: true,
6770
import_map: "1,2,3",
71+
lint: false,
6872
} as ConfigurationField);
6973
});
7074

@@ -87,5 +91,6 @@ test("core / configuration / update", async () => {
8791
enable: true,
8892
unstable: false,
8993
import_map: null,
94+
lint: false,
9095
} as ConfigurationField);
9196
});

core/configuration.ts

+4
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ export const DenoPluginConfigurationField: (keyof ConfigurationField)[] = [
1111
"enable",
1212
"unstable",
1313
"import_map",
14+
"lint",
1415
];
1516

1617
export type ConfigurationField = {
1718
enable?: boolean;
1819
unstable?: boolean;
1920
import_map?: string | null;
21+
lint?: boolean;
2022
};
2123

2224
interface ConfigurationInterface {
@@ -31,6 +33,7 @@ export class Configuration implements ConfigurationInterface {
3133
enable: false,
3234
unstable: false,
3335
import_map: null,
36+
lint: false,
3437
};
3538

3639
private readonly _configUpdatedListeners = new Set<() => void>();
@@ -76,6 +79,7 @@ export class Configuration implements ConfigurationInterface {
7679
// Make sure the type of each configuration item is correct
7780
this._configuration.enable = !!this._configuration.enable;
7881
this._configuration.unstable = !!this._configuration.unstable;
82+
this._configuration.lint = !!this._configuration.lint;
7983
this._configuration.import_map = this._configuration.import_map
8084
? this._configuration.import_map + ""
8185
: null;

package.json

+10
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@
122122
true,
123123
false
124124
]
125+
},
126+
"deno.lint": {
127+
"type": "boolean",
128+
"default": false,
129+
"markdownDescription": "Controls if `deno lint` is enabled. It is currently experimental, so make sure `deno.unstable: true` is enabled. \n\n**Not recommended in global configuration**",
130+
"scope": "resource",
131+
"examples": [
132+
true,
133+
false
134+
]
125135
}
126136
}
127137
},

server/src/bridge.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { Request } from "../../core/const";
66
type Configuration = {
77
enable: boolean;
88
import_map?: string;
9+
unstable?: boolean;
10+
lint?: boolean;
911
};
1012

1113
/**

server/src/deno.ts

+93-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Readable } from "stream";
33
import execa from "execa";
44
import which from "which";
55
import * as semver from "semver";
6+
import { Cache } from "../../core/cache";
67

78
type Version = {
89
deno: string;
@@ -15,6 +16,34 @@ type FormatOptions = {
1516
cwd: string;
1617
};
1718

19+
interface LintLocation {
20+
line: number; // one base number
21+
col: number; // zero base number
22+
}
23+
24+
interface LintDiagnostic {
25+
code: string;
26+
filename: string;
27+
message: string;
28+
range: {
29+
start: LintLocation;
30+
end: LintLocation;
31+
};
32+
}
33+
34+
interface LintError {
35+
file_path: string;
36+
message: string;
37+
}
38+
39+
interface LintOutput {
40+
diagnostics: LintDiagnostic[];
41+
errors: LintError[];
42+
}
43+
44+
// caching Deno lint's rules for 120s or 100 referenced times
45+
const denoLintRulesCache = Cache.create<string[]>(1000 * 120, 100);
46+
1847
class Deno {
1948
public version!: Version | void;
2049
public executablePath!: string | void;
@@ -33,25 +62,23 @@ class Deno {
3362
return;
3463
}
3564

36-
// If the currently used Deno is less than 0.33.0
65+
// If the currently used Deno is less than 1.3.3
3766
// We will give an warning to upgrade.
38-
const minimumDenoVersion = "0.35.0";
67+
const minimumDenoVersion = "1.3.3";
3968
if (!semver.gte(this.version.deno, minimumDenoVersion)) {
4069
throw new Error(`Please upgrade to Deno ${minimumDenoVersion} or above.`);
4170
}
4271
}
4372
public async getTypes(unstable: boolean): Promise<Buffer> {
4473
const { stdout } = await execa(this.executablePath as string, [
4574
"types",
46-
...(unstable && this.version && semver.gte(this.version.deno, "0.43.0")
47-
? ["--unstable"]
48-
: []),
75+
...(unstable ? ["--unstable"] : []),
4976
]);
5077

5178
return Buffer.from(stdout, "utf8");
5279
}
5380
// format code
54-
// echo "console.log(123)" | deno fmt --stdin
81+
// echo "console.log(123)" | deno fmt -
5582
public async format(code: string, options: FormatOptions): Promise<string> {
5683
const reader = Readable.from([code]);
5784

@@ -72,22 +99,71 @@ class Deno {
7299
resolve(stdout);
73100
}
74101
});
75-
subprocess.on("error", (err: Error) => {
76-
reject(err);
77-
});
78-
subprocess.stdout?.on("data", (data: Buffer) => {
79-
stdout += data;
80-
});
81-
82-
subprocess.stderr?.on("data", (data: Buffer) => {
83-
stderr += data;
84-
});
85-
102+
subprocess.on("error", (err: Error) => reject(err));
103+
subprocess.stdout?.on("data", (data: Buffer) => (stdout += data));
104+
subprocess.stderr?.on("data", (data: Buffer) => (stderr += data));
86105
subprocess.stdin && reader.pipe(subprocess.stdin);
87106
})) as string;
88107

89108
return formattedCode;
90109
}
110+
111+
public async getLintRules(): Promise<string[]> {
112+
const cachedRules = denoLintRulesCache.get();
113+
if (cachedRules) {
114+
return cachedRules;
115+
}
116+
const subprocess = execa(
117+
this.executablePath as string,
118+
["lint", "--unstable", "--rules"],
119+
{
120+
stdout: "pipe",
121+
}
122+
);
123+
124+
const output = await new Promise<string>((resolve, reject) => {
125+
let stdout = "";
126+
subprocess.on("exit", () => resolve(stdout));
127+
subprocess.on("error", (err: Error) => reject(err));
128+
subprocess.stdout?.on("data", (data: Buffer) => (stdout += data));
129+
});
130+
131+
const rules = output
132+
.split("\n")
133+
.map((v) => v.trim())
134+
.filter((v) => v.startsWith("-"))
135+
.map((v) => v.replace(/^-\s+/, ""));
136+
137+
denoLintRulesCache.set(rules);
138+
139+
return rules;
140+
}
141+
142+
// lint code
143+
// echo "console.log(123)" | deno lint --unstable --json -
144+
public async lint(code: string): Promise<LintOutput> {
145+
const reader = Readable.from([code]);
146+
147+
const subprocess = execa(
148+
this.executablePath as string,
149+
["lint", "--unstable", "--json", "-"],
150+
{
151+
stdin: "pipe",
152+
stderr: "pipe",
153+
}
154+
);
155+
156+
const output = await new Promise<string>((resolve, reject) => {
157+
let stderr = "";
158+
subprocess.on("exit", () => resolve(stderr));
159+
subprocess.on("error", (err: Error) => reject(err));
160+
subprocess.stderr?.on("data", (data: Buffer) => (stderr += data));
161+
subprocess.stdin && reader.pipe(subprocess.stdin);
162+
});
163+
164+
return JSON.parse(output) as LintOutput;
165+
}
166+
91167
private async getExecutablePath(): Promise<string | undefined> {
92168
const denoPath = await which("deno").catch(() =>
93169
Promise.resolve(undefined)

0 commit comments

Comments
 (0)