@knip/language-server 3.0.5 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/package.json +5 -4
- package/src/diagnostics.js +61 -13
- package/src/server.js +1 -1
- package/src/types.d.ts +3 -1
- package/test/diagnostics.test.js +149 -0
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knip/language-server",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/webpro-nl/knip.git",
|
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
"./constants": "./src/constants.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"vscode-languageserver": "^10.
|
|
22
|
-
"vscode-languageserver-textdocument": "^1.0.
|
|
23
|
-
"knip": "^6.
|
|
21
|
+
"vscode-languageserver": "^10.1.1",
|
|
22
|
+
"vscode-languageserver-textdocument": "^1.0.14",
|
|
23
|
+
"knip": "^6.35.0"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": "^20.19.0 || >=22.12.0"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
+
"test": "node --test",
|
|
29
30
|
"release": "release-it -c ../../.release-it.json"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/src/diagnostics.js
CHANGED
|
@@ -5,14 +5,14 @@ import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver/node';
|
|
|
5
5
|
* @import { Diagnostic } from 'vscode-languageserver';
|
|
6
6
|
* @import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
7
7
|
* @import { Config } from './types.js';
|
|
8
|
-
* @import { Issue, Rules } from 'knip/session';
|
|
8
|
+
* @import { Issue, IssueType, Rules } from 'knip/session';
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/** Superseded by `editor.severity`; kept until the settings are removed */
|
|
11
12
|
const dimmedIssueTypes = {
|
|
12
13
|
exports: 'dimExports',
|
|
13
14
|
types: 'dimTypes',
|
|
14
15
|
enumMembers: 'dimEnumMembers',
|
|
15
|
-
classMembers: 'dimClassMembers',
|
|
16
16
|
duplicates: 'dimDuplicates',
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -23,6 +23,57 @@ const SEVERITY = {
|
|
|
23
23
|
hint: DiagnosticSeverity.Hint,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const ABSOLUTE_SEVERITY = {
|
|
27
|
+
error: DiagnosticSeverity.Error,
|
|
28
|
+
warning: DiagnosticSeverity.Warning,
|
|
29
|
+
information: DiagnosticSeverity.Information,
|
|
30
|
+
hint: DiagnosticSeverity.Hint,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Loudest to quietest, so `downgrade` and `upgrade` can step along it */
|
|
34
|
+
const SEVERITY_ORDER = [
|
|
35
|
+
DiagnosticSeverity.Error,
|
|
36
|
+
DiagnosticSeverity.Warning,
|
|
37
|
+
DiagnosticSeverity.Information,
|
|
38
|
+
DiagnosticSeverity.Hint,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {DiagnosticSeverity} severity
|
|
43
|
+
* @param {number} steps
|
|
44
|
+
* @returns {DiagnosticSeverity}
|
|
45
|
+
*/
|
|
46
|
+
const step = (severity, steps) => {
|
|
47
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
48
|
+
if (index === -1) return severity;
|
|
49
|
+
return SEVERITY_ORDER[Math.min(SEVERITY_ORDER.length - 1, Math.max(0, index + steps))];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Editor-only: `rules` decides what fails a run, this decides how the editor renders it.
|
|
54
|
+
* Most specific wins: the issue type, then a dim setting, then `*`, then `rules`.
|
|
55
|
+
* @param {IssueType} type
|
|
56
|
+
* @param {DiagnosticSeverity} base severity `rules` would give this issue type
|
|
57
|
+
* @param {Config} config
|
|
58
|
+
* @returns {DiagnosticSeverity}
|
|
59
|
+
*/
|
|
60
|
+
const resolveSeverity = (type, base, config) => {
|
|
61
|
+
const configured = config.editor.severity;
|
|
62
|
+
const isScalar = typeof configured === 'string';
|
|
63
|
+
|
|
64
|
+
const dimSetting = dimmedIssueTypes[type];
|
|
65
|
+
const isDimmed = dimSetting ? config.editor.exports.highlight[dimSetting] === true : false;
|
|
66
|
+
|
|
67
|
+
const value =
|
|
68
|
+
(isScalar ? undefined : configured?.[type]) ??
|
|
69
|
+
(isDimmed ? 'hint' : undefined) ??
|
|
70
|
+
(isScalar ? configured : configured?.['*']);
|
|
71
|
+
|
|
72
|
+
if (value === 'downgrade') return step(base, 1);
|
|
73
|
+
if (value === 'upgrade') return step(base, -1);
|
|
74
|
+
return ABSOLUTE_SEVERITY[value] ?? base;
|
|
75
|
+
};
|
|
76
|
+
|
|
26
77
|
/**
|
|
27
78
|
* @param {Issue} issue
|
|
28
79
|
* @param {Rules} rules
|
|
@@ -31,9 +82,14 @@ const SEVERITY = {
|
|
|
31
82
|
* @returns {Diagnostic}
|
|
32
83
|
*/
|
|
33
84
|
export const issueToDiagnostic = (issue, rules, config, document) => {
|
|
85
|
+
// unused files are informational by default: the whole file is the finding, not a symbol in it
|
|
86
|
+
const base = issue.type === 'files' ? DiagnosticSeverity.Information : SEVERITY[rules[issue.type]];
|
|
87
|
+
const severity = resolveSeverity(issue.type, base, config);
|
|
88
|
+
|
|
34
89
|
if (issue.type === 'files' && document) {
|
|
35
90
|
return {
|
|
36
|
-
severity
|
|
91
|
+
severity,
|
|
92
|
+
tags: severity === DiagnosticSeverity.Hint ? [DiagnosticTag.Unnecessary] : undefined,
|
|
37
93
|
range: {
|
|
38
94
|
start: { line: 0, character: 0 },
|
|
39
95
|
end: { line: 0, character: 1 },
|
|
@@ -44,16 +100,8 @@ export const issueToDiagnostic = (issue, rules, config, document) => {
|
|
|
44
100
|
};
|
|
45
101
|
}
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/** @type {DiagnosticTag[]} */
|
|
51
|
-
const tags = [];
|
|
52
|
-
|
|
53
|
-
if (dimmedIssueTypes[issue.type] && config.editor.exports.highlight[dimmedIssueTypes[issue.type]]) {
|
|
54
|
-
severity = DiagnosticSeverity.Hint;
|
|
55
|
-
tags.push(DiagnosticTag.Unnecessary);
|
|
56
|
-
}
|
|
103
|
+
// a hint is nearly invisible without it, and this is what makes `hint` fade the code
|
|
104
|
+
const tags = severity === DiagnosticSeverity.Hint ? [DiagnosticTag.Unnecessary] : [];
|
|
57
105
|
|
|
58
106
|
const line = Math.max(0, (issue.line ?? 1) - 1);
|
|
59
107
|
const start = Math.max(0, (issue.col ?? 0) - 1);
|
package/src/server.js
CHANGED
|
@@ -43,6 +43,7 @@ function readKnipVersion(resolvedPath) {
|
|
|
43
43
|
const DEFAULT_CONFIG = {
|
|
44
44
|
deferSession: false,
|
|
45
45
|
editor: {
|
|
46
|
+
severity: 'default',
|
|
46
47
|
exports: {
|
|
47
48
|
codelens: { enabled: true },
|
|
48
49
|
hover: { enabled: true, includeImportLocationSnippet: false, maxSnippets: 10, timeout: 300 },
|
|
@@ -51,7 +52,6 @@ const DEFAULT_CONFIG = {
|
|
|
51
52
|
dimExports: false,
|
|
52
53
|
dimTypes: false,
|
|
53
54
|
dimEnumMembers: false,
|
|
54
|
-
dimClassMembers: false,
|
|
55
55
|
dimDuplicates: false,
|
|
56
56
|
},
|
|
57
57
|
},
|
package/src/types.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Issue, IssueType } from 'knip/session';
|
|
2
2
|
|
|
3
|
+
export type EditorSeverity = 'default' | 'error' | 'warning' | 'information' | 'hint' | 'downgrade' | 'upgrade';
|
|
4
|
+
|
|
3
5
|
export type Config = {
|
|
4
6
|
deferSession: boolean;
|
|
5
7
|
configFilePath?: string;
|
|
6
8
|
editor: {
|
|
9
|
+
severity: EditorSeverity | Partial<Record<IssueType | '*', EditorSeverity>>;
|
|
7
10
|
exports: {
|
|
8
11
|
codelens: {
|
|
9
12
|
enabled: boolean;
|
|
@@ -22,7 +25,6 @@ export type Config = {
|
|
|
22
25
|
dimExports: boolean;
|
|
23
26
|
dimTypes: boolean;
|
|
24
27
|
dimEnumMembers: boolean;
|
|
25
|
-
dimClassMembers: boolean;
|
|
26
28
|
dimDuplicates: boolean;
|
|
27
29
|
};
|
|
28
30
|
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { ISSUE_TYPES } from 'knip/session';
|
|
5
|
+
import { DiagnosticSeverity, DiagnosticTag } from 'vscode-languageserver/node';
|
|
6
|
+
import { issueToDiagnostic } from '../src/diagnostics.js';
|
|
7
|
+
|
|
8
|
+
const manifest = JSON.parse(readFileSync(new URL('../../vscode-knip/package.json', import.meta.url), 'utf8'));
|
|
9
|
+
const editorSettings = manifest.contributes.configuration.find(section => section.title === 'Editor').properties;
|
|
10
|
+
|
|
11
|
+
const dimSettings = Object.keys(editorSettings)
|
|
12
|
+
.filter(key => key.includes('.highlight.dim'))
|
|
13
|
+
.map(key => key.slice(key.lastIndexOf('.') + 1));
|
|
14
|
+
|
|
15
|
+
const toIssueType = setting => setting.replace(/^dim/, '').replace(/^./, first => first.toLowerCase());
|
|
16
|
+
|
|
17
|
+
const NO_DIM = Object.fromEntries(dimSettings.map(setting => [setting, false]));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {unknown} [severity]
|
|
21
|
+
* @param {Record<string, boolean>} [highlight]
|
|
22
|
+
*/
|
|
23
|
+
const config = (severity, highlight = NO_DIM) => ({ editor: { severity, exports: { highlight } } });
|
|
24
|
+
|
|
25
|
+
const rules = { exports: 'error', types: 'warn', files: 'error', enumMembers: 'error', duplicates: 'error' };
|
|
26
|
+
|
|
27
|
+
const unusedExport = { type: 'exports', symbol: 'unusedThing', line: 3, col: 14, filePath: '/project/module.ts' };
|
|
28
|
+
const unusedType = { ...unusedExport, type: 'types' };
|
|
29
|
+
const unusedFile = { type: 'files', symbol: 'unused.ts', filePath: '/project/unused.ts' };
|
|
30
|
+
|
|
31
|
+
const document = { getText: () => '' };
|
|
32
|
+
|
|
33
|
+
const severityOf = (issue, cfg, doc) => issueToDiagnostic(issue, rules, cfg, doc).severity;
|
|
34
|
+
|
|
35
|
+
test('severity follows the rules by default', () => {
|
|
36
|
+
assert.equal(severityOf(unusedExport, config('default')), DiagnosticSeverity.Error);
|
|
37
|
+
assert.equal(severityOf(unusedType, config('default')), DiagnosticSeverity.Warning);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('severity falls back to the rules when a client sends no setting', () => {
|
|
41
|
+
assert.equal(severityOf(unusedExport, config(undefined)), DiagnosticSeverity.Error);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('a scalar applies to every issue type', () => {
|
|
45
|
+
assert.equal(severityOf(unusedExport, config('warning')), DiagnosticSeverity.Warning);
|
|
46
|
+
assert.equal(severityOf(unusedType, config('information')), DiagnosticSeverity.Information);
|
|
47
|
+
assert.equal(severityOf(unusedFile, config('hint'), document), DiagnosticSeverity.Hint);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a scalar raises issue types the rules made a warning', () => {
|
|
51
|
+
assert.equal(severityOf(unusedType, config('error')), DiagnosticSeverity.Error);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('downgrade and upgrade step relative to the rules', () => {
|
|
55
|
+
assert.equal(severityOf(unusedExport, config('downgrade')), DiagnosticSeverity.Warning);
|
|
56
|
+
assert.equal(severityOf(unusedType, config('downgrade')), DiagnosticSeverity.Information);
|
|
57
|
+
assert.equal(severityOf(unusedType, config('upgrade')), DiagnosticSeverity.Error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('downgrade and upgrade stop at the ends of the scale', () => {
|
|
61
|
+
assert.equal(severityOf(unusedExport, config('upgrade')), DiagnosticSeverity.Error);
|
|
62
|
+
assert.equal(severityOf(unusedFile, config({ files: 'hint' }), document), DiagnosticSeverity.Hint);
|
|
63
|
+
assert.equal(severityOf(unusedFile, config({ files: 'downgrade' }), document), DiagnosticSeverity.Hint);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a map keyed by issue type beats the wildcard', () => {
|
|
67
|
+
const cfg = config({ '*': 'hint', exports: 'error' });
|
|
68
|
+
|
|
69
|
+
assert.equal(severityOf(unusedExport, cfg), DiagnosticSeverity.Error);
|
|
70
|
+
assert.equal(severityOf(unusedType, cfg), DiagnosticSeverity.Hint);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('the wildcard alone covers every issue type', () => {
|
|
74
|
+
const cfg = config({ '*': 'warning' });
|
|
75
|
+
|
|
76
|
+
assert.equal(severityOf(unusedExport, cfg), DiagnosticSeverity.Warning);
|
|
77
|
+
assert.equal(severityOf(unusedType, cfg), DiagnosticSeverity.Warning);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('issue types absent from a map follow the rules', () => {
|
|
81
|
+
const cfg = config({ exports: 'hint' });
|
|
82
|
+
|
|
83
|
+
assert.equal(severityOf(unusedType, cfg), DiagnosticSeverity.Warning);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('unused files are information by default', () => {
|
|
87
|
+
assert.equal(severityOf(unusedFile, config('default'), document), DiagnosticSeverity.Information);
|
|
88
|
+
assert.equal(severityOf(unusedFile, config({ '*': 'warning' }), document), DiagnosticSeverity.Warning);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a hint fades the code, whichever value produced it', () => {
|
|
92
|
+
for (const severity of ['hint', { exports: 'hint' }, { '*': 'hint' }]) {
|
|
93
|
+
const diagnostic = issueToDiagnostic(unusedExport, rules, config(severity));
|
|
94
|
+
|
|
95
|
+
assert.equal(diagnostic.severity, DiagnosticSeverity.Hint);
|
|
96
|
+
assert.deepEqual(diagnostic.tags, [DiagnosticTag.Unnecessary]);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('every contributed dim setting names a real issue type, and dims it', () => {
|
|
101
|
+
assert.ok(dimSettings.length > 0, 'no dim settings found in the extension manifest');
|
|
102
|
+
|
|
103
|
+
for (const setting of dimSettings) {
|
|
104
|
+
const type = toIssueType(setting);
|
|
105
|
+
assert.ok(ISSUE_TYPES.includes(type), `${setting} names "${type}", which is not a Knip issue type`);
|
|
106
|
+
|
|
107
|
+
const issue = { ...unusedExport, type };
|
|
108
|
+
const diagnostic = issueToDiagnostic(issue, rules, config('default', { ...NO_DIM, [setting]: true }));
|
|
109
|
+
|
|
110
|
+
assert.equal(diagnostic.severity, DiagnosticSeverity.Hint, setting);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a dim setting still dims, and beats the wildcard', () => {
|
|
115
|
+
const cfg = config({ '*': 'error' }, { ...NO_DIM, dimExports: true });
|
|
116
|
+
const diagnostic = issueToDiagnostic(unusedExport, rules, cfg);
|
|
117
|
+
|
|
118
|
+
assert.equal(diagnostic.severity, DiagnosticSeverity.Hint);
|
|
119
|
+
assert.deepEqual(diagnostic.tags, [DiagnosticTag.Unnecessary]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('an entry for the issue type beats a dim setting', () => {
|
|
123
|
+
const cfg = config({ exports: 'error' }, { ...NO_DIM, dimExports: true });
|
|
124
|
+
|
|
125
|
+
assert.equal(severityOf(unusedExport, cfg), DiagnosticSeverity.Error);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('a diagnostic carries the position and description of its issue', () => {
|
|
129
|
+
const diagnostic = issueToDiagnostic(unusedExport, rules, config('default'));
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(diagnostic.range, {
|
|
132
|
+
start: { line: 2, character: 13 },
|
|
133
|
+
end: { line: 2, character: 24 },
|
|
134
|
+
});
|
|
135
|
+
assert.equal(diagnostic.message, 'Unused export: unusedThing');
|
|
136
|
+
assert.equal(diagnostic.source, 'knip');
|
|
137
|
+
assert.equal(diagnostic.code, 'exports');
|
|
138
|
+
assert.equal(diagnostic.tags, undefined);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('an unused file is reported at the top of the file', () => {
|
|
142
|
+
const diagnostic = issueToDiagnostic(unusedFile, rules, config('default'), document);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(diagnostic.range, {
|
|
145
|
+
start: { line: 0, character: 0 },
|
|
146
|
+
end: { line: 0, character: 1 },
|
|
147
|
+
});
|
|
148
|
+
assert.equal(diagnostic.message, 'Unused file');
|
|
149
|
+
});
|