@@ -5,8 +5,27 @@ import { DiagnosticsProvider } from '../../interfaces';
55import { LSAndTSDocResolver } from '../LSAndTSDocResolver' ;
66import { convertRange , getDiagnosticTag , mapSeverity } from '../utils' ;
77import { 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
1130export 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) {
135173function 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 */
174202function 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 */
181209function 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