Skip to content

Commit 255d9dc

Browse files
authored
(fix) Silence diagnostics for no side effects variables in reactive blocks (#1286)
#1277 For diagnostics "Left side of comma operator is unused and has no side effects.", this code analyzes the AST to determine whether the errors point to a let variable in a reactive statement. In this case, we assume this is purposeful reactive block hinting and suppress the error. New diagnostics are emitted for each non let identifiers that exists in the span of the original diagnostic. To fix the main case, an AST based isReactiveStatement util function is introduced. The "Unused label." warnings for $: of reactive statements used a weak heuristic, so the warning suppression strategy was switched to using the AST based function too.
1 parent 016e475 commit 255d9dc

File tree

20 files changed

+524
-49
lines changed

20 files changed

+524
-49
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
'import/no-unused-modules': 'off',
1919
'import/no-deprecated': 'off',
2020
// project-specific settings
21+
curly: [2, 'all'],
2122
'max-len': 'off', // handled by prettier
2223
'no-trailing-spaces': 'error',
2324
'one-var': ['error', 'never'],

packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts

Lines changed: 147 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,27 @@ import { DiagnosticsProvider } from '../../interfaces';
55
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
66
import { convertRange, getDiagnosticTag, mapSeverity } from '../utils';
77
import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot';
8-
import { isInGeneratedCode, isAfterSvelte2TsxPropsReturn } from './utils';
9-
import { regexIndexOf, swapRangeStartEndIfNecessary } from '../../../utils';
8+
import {
9+
isInGeneratedCode,
10+
isAfterSvelte2TsxPropsReturn,
11+
findNodeAtSpan,
12+
isReactiveStatement,
13+
isInReactiveStatement,
14+
gatherIdentifiers
15+
} from './utils';
16+
import { not, flatten, passMap, regexIndexOf, swapRangeStartEndIfNecessary } from '../../../utils';
17+
18+
enum DiagnosticCode {
19+
MODIFIERS_CANNOT_APPEAR_HERE = 1184, // "Modifiers cannot appear here."
20+
USED_BEFORE_ASSIGNED = 2454, // "Variable '{0}' is used before being assigned."
21+
JSX_ELEMENT_DOES_NOT_SUPPORT_ATTRIBUTES = 2607, // "JSX element class does not support attributes because it does not have a '{0}' property."
22+
CANNOT_BE_USED_AS_JSX_COMPONENT = 2786, // "'{0}' cannot be used as a JSX component."
23+
NOOP_IN_COMMAS = 2695, // "Left side of comma operator is unused and has no side effects."
24+
NEVER_READ = 6133, // "'{0}' is declared but its value is never read."
25+
ALL_IMPORTS_UNUSED = 6192, // "All imports in import declaration are unused."
26+
UNUSED_LABEL = 7028, // "Unused label."
27+
DUPLICATED_JSX_ATTRIBUTES = 17001 // "JSX elements cannot have multiple attributes with the same name."
28+
}
1029

1130
export class DiagnosticsProviderImpl implements DiagnosticsProvider {
1231
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
@@ -39,16 +58,19 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider {
3958
];
4059
}
4160

42-
const diagnostics: ts.Diagnostic[] = [
61+
const fragment = await tsDoc.getFragment();
62+
63+
let diagnostics: ts.Diagnostic[] = [
4364
...lang.getSyntacticDiagnostics(tsDoc.filePath),
4465
...lang.getSuggestionDiagnostics(tsDoc.filePath),
4566
...lang.getSemanticDiagnostics(tsDoc.filePath)
4667
];
47-
48-
const fragment = await tsDoc.getFragment();
68+
diagnostics = diagnostics
69+
.filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength())))
70+
.filter(not(isUnusedReactiveStatementLabel));
71+
diagnostics = resolveNoopsInReactiveStatements(lang, diagnostics);
4972

5073
return diagnostics
51-
.filter(isNotGenerated(tsDoc.getText(0, tsDoc.getLength())))
5274
.map<Diagnostic>((diagnostic) => ({
5375
range: convertRange(tsDoc, diagnostic),
5476
severity: mapSeverity(diagnostic.category),
@@ -105,6 +127,23 @@ function mapRange(
105127
};
106128
}
107129

130+
function findDiagnosticNode(diagnostic: ts.Diagnostic) {
131+
const { file, start, length } = diagnostic;
132+
if (!file || !start || !length) {
133+
return;
134+
}
135+
const span = { start, length };
136+
return findNodeAtSpan(file, span);
137+
}
138+
139+
function copyDiagnosticAndChangeNode(diagnostic: ts.Diagnostic) {
140+
return (node: ts.Node) => ({
141+
...diagnostic,
142+
start: node.getStart(),
143+
length: node.getWidth()
144+
});
145+
}
146+
108147
/**
109148
* In some rare cases mapping of diagnostics does not work and produces negative lines.
110149
* We filter out these diagnostics with negative lines because else the LSP
@@ -121,7 +160,6 @@ function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
121160
return (diagnostic: Diagnostic) => {
122161
return (
123162
isNoJsxCannotHaveMultipleAttrsError(diagnostic) &&
124-
isNoUnusedLabelWarningForReactiveStatement(diagnostic) &&
125163
isNoUsedBeforeAssigned(diagnostic, text, tsDoc) &&
126164
(!usesPug || isNoPugFalsePositive(diagnostic, document))
127165
);
@@ -135,8 +173,8 @@ function isNoFalsePositive(document: Document, tsDoc: SvelteDocumentSnapshot) {
135173
function isNoPugFalsePositive(diagnostic: Diagnostic, document: Document): boolean {
136174
return (
137175
!isRangeInTag(diagnostic.range, document.templateInfo) &&
138-
diagnostic.code !== 6133 &&
139-
diagnostic.code !== 6192
176+
diagnostic.code !== DiagnosticCode.NEVER_READ &&
177+
diagnostic.code !== DiagnosticCode.ALL_IMPORTS_UNUSED
140178
);
141179
}
142180

@@ -150,36 +188,26 @@ function isNoUsedBeforeAssigned(
150188
text: string,
151189
tsDoc: SvelteDocumentSnapshot
152190
): boolean {
153-
if (diagnostic.code !== 2454) {
191+
if (diagnostic.code !== DiagnosticCode.USED_BEFORE_ASSIGNED) {
154192
return true;
155193
}
156194

157195
return !tsDoc.hasProp(getTextInRange(diagnostic.range, text));
158196
}
159197

160-
/**
161-
* Unused label warning when using reactive statement (`$: a = ...`)
162-
*/
163-
function isNoUnusedLabelWarningForReactiveStatement(diagnostic: Diagnostic) {
164-
return (
165-
diagnostic.code !== 7028 ||
166-
diagnostic.range.end.character - 1 !== diagnostic.range.start.character
167-
);
168-
}
169-
170198
/**
171199
* Jsx cannot have multiple attributes with same name,
172200
* but that's allowed for svelte
173201
*/
174202
function isNoJsxCannotHaveMultipleAttrsError(diagnostic: Diagnostic) {
175-
return diagnostic.code !== 17001;
203+
return diagnostic.code !== DiagnosticCode.DUPLICATED_JSX_ATTRIBUTES;
176204
}
177205

178206
/**
179207
* Some diagnostics have JSX-specific nomenclature. Enhance them for more clarity.
180208
*/
181209
function enhanceIfNecessary(diagnostic: Diagnostic): Diagnostic {
182-
if (diagnostic.code === 2786) {
210+
if (diagnostic.code === DiagnosticCode.CANNOT_BE_USED_AS_JSX_COMPONENT) {
183211
return {
184212
...diagnostic,
185213
message:
@@ -196,7 +224,7 @@ function enhanceIfNecessary(diagnostic: Diagnostic): Diagnostic {
196224
};
197225
}
198226

199-
if (diagnostic.code === 2607) {
227+
if (diagnostic.code === DiagnosticCode.JSX_ELEMENT_DOES_NOT_SUPPORT_ATTRIBUTES) {
200228
return {
201229
...diagnostic,
202230
message:
@@ -207,7 +235,7 @@ function enhanceIfNecessary(diagnostic: Diagnostic): Diagnostic {
207235
};
208236
}
209237

210-
if (diagnostic.code === 1184) {
238+
if (diagnostic.code === DiagnosticCode.MODIFIERS_CANNOT_APPEAR_HERE) {
211239
return {
212240
...diagnostic,
213241
message:
@@ -239,3 +267,98 @@ function isNotGenerated(text: string) {
239267
return !isInGeneratedCode(text, diagnostic.start, diagnostic.start + diagnostic.length);
240268
};
241269
}
270+
271+
function isUnusedReactiveStatementLabel(diagnostic: ts.Diagnostic) {
272+
if (diagnostic.code !== DiagnosticCode.UNUSED_LABEL) {
273+
return false;
274+
}
275+
276+
const diagNode = findDiagnosticNode(diagnostic);
277+
if (!diagNode) {
278+
return false;
279+
}
280+
281+
// TS warning targets the identifier
282+
if (!ts.isIdentifier(diagNode)) {
283+
return false;
284+
}
285+
286+
if (!diagNode.parent) {
287+
return false;
288+
}
289+
return isReactiveStatement(diagNode.parent);
290+
}
291+
292+
/**
293+
* Checks if diagnostics should be ignored because they report an unused expression* in
294+
* a reactive statement, and those actually have side effects in Svelte (hinting deps).
295+
*
296+
* $: x, update()
297+
*
298+
* Only `let` (i.e. reactive) variables are ignored. For the others, new diagnostics are
299+
* emitted, centered on the (non reactive) identifiers in the initial warning.
300+
*/
301+
function resolveNoopsInReactiveStatements(lang: ts.LanguageService, diagnostics: ts.Diagnostic[]) {
302+
const isLet = (file: ts.SourceFile) => (node: ts.Node) => {
303+
const defs = lang.getDefinitionAtPosition(file.fileName, node.getStart());
304+
return !!defs && defs.some((def) => def.fileName === file.fileName && def.kind === 'let');
305+
};
306+
307+
const expandRemainingNoopWarnings = (diagnostic: ts.Diagnostic): void | ts.Diagnostic[] => {
308+
const { code, file } = diagnostic;
309+
310+
// guard: missing info
311+
if (!file) {
312+
return;
313+
}
314+
315+
// guard: not target error
316+
const isNoopDiag = code === DiagnosticCode.NOOP_IN_COMMAS;
317+
if (!isNoopDiag) {
318+
return;
319+
}
320+
321+
const diagNode = findDiagnosticNode(diagnostic);
322+
if (!diagNode) {
323+
return;
324+
}
325+
326+
if (!isInReactiveStatement(diagNode)) {
327+
return;
328+
}
329+
330+
return (
331+
// for all identifiers in diagnostic node
332+
gatherIdentifiers(diagNode)
333+
// ignore `let` (i.e. reactive) variables
334+
.filter(not(isLet(file)))
335+
// and create targeted diagnostics just for the remaining ids
336+
.map(copyDiagnosticAndChangeNode(diagnostic))
337+
);
338+
};
339+
340+
const expandedDiagnostics = flatten(passMap(diagnostics, expandRemainingNoopWarnings));
341+
return expandedDiagnostics.length === diagnostics.length
342+
? expandedDiagnostics
343+
: // This can generate duplicate diagnostics
344+
expandedDiagnostics.filter(dedupDiagnostics());
345+
}
346+
347+
function dedupDiagnostics() {
348+
const hashDiagnostic = (diag: ts.Diagnostic) =>
349+
[diag.start, diag.length, diag.category, diag.source, diag.code]
350+
.map((x) => JSON.stringify(x))
351+
.join(':');
352+
353+
const known = new Set();
354+
355+
return (diag: ts.Diagnostic) => {
356+
const key = hashDiagnostic(diag);
357+
if (known.has(key)) {
358+
return false;
359+
} else {
360+
known.add(key);
361+
return true;
362+
}
363+
};
364+
}

0 commit comments

Comments
 (0)