Skip to content

Commit 4bbda07

Browse files
eseligerchristoph-sg
authored andcommitted
1 parent 682525e commit 4bbda07

4 files changed

Lines changed: 136 additions & 9 deletions

File tree

src/ProjectIndexer.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import * as assert from 'uvu/assert'
88

99
import { GlobalCache, ProjectOptions } from './CommandLineOptions'
1010
import {
11+
deduplicateOccurrences,
1112
languageForFileName,
1213
prettyMilliseconds,
1314
ProjectIndexer,
1415
} from './ProjectIndexer'
16+
import * as scip from './scip'
1517

1618
function minute(x: number): number {
1719
return x * 60 * 1000
@@ -58,6 +60,7 @@ test('only deduplicates documents after successful emission', () => {
5860
sources: new Map(),
5961
parsedCommandLines: new Map(),
6062
indexedFiles: new Set(),
63+
sourceInfos: new Map(),
6164
}
6265
const options: ProjectOptions = {
6366
cwd: projectRoot,
@@ -91,4 +94,33 @@ test('only deduplicates documents after successful emission', () => {
9194
}
9295
})
9396

97+
test('Svelte occurrence deduplication preserves definition metadata', () => {
98+
const reference = new scip.scip.Occurrence({
99+
range: [1, 2, 3],
100+
symbol: 'local 0',
101+
})
102+
const definition = new scip.scip.Occurrence({
103+
range: [1, 2, 3],
104+
enclosing_range: [1, 0, 5, 0],
105+
symbol: 'local 0',
106+
symbol_roles: scip.scip.SymbolRole.Definition,
107+
diagnostics: [
108+
new scip.scip.Diagnostic({ message: 'definition diagnostic' }),
109+
],
110+
})
111+
const document = new scip.scip.Document({
112+
occurrences: [reference, definition],
113+
})
114+
115+
deduplicateOccurrences(document)
116+
117+
assert.is(document.occurrences.length, 1)
118+
assert.is(document.occurrences[0], definition)
119+
assert.equal(document.occurrences[0].enclosing_range, [1, 0, 5, 0])
120+
assert.is(
121+
document.occurrences[0].diagnostics[0].message,
122+
'definition diagnostic'
123+
)
124+
})
125+
94126
test.run()

src/ProjectIndexer.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ function createCompilerHost(
1616
cache: GlobalCache,
1717
compilerOptions: ts.CompilerOptions,
1818
projectOptions: ProjectOptions,
19-
hasSvelte: boolean
19+
hasSvelte: boolean,
20+
sourceInfos: Map<ts.SourceFile, SourceInfo>
2021
): ts.CompilerHost {
2122
const host = ts.createCompilerHost(compilerOptions)
2223
if (!hasSvelte && !projectOptions.globalCaches) {
2324
return host
2425
}
2526
const hostCopy = { ...host }
2627
const svelte = hasSvelte
27-
? new SvelteSupport(hostCopy, compilerOptions, cache.sourceInfos)
28+
? new SvelteSupport(hostCopy, compilerOptions, sourceInfos)
2829
: undefined
2930
if (svelte) {
3031
host.fileExists = fileName => svelte.fileExists(fileName)
@@ -118,7 +119,16 @@ export class ProjectIndexer {
118119
cache: GlobalCache
119120
) {
120121
const hasSvelte = config.fileNames.some(isSvelteFile)
121-
const host = createCompilerHost(cache, config.options, options, hasSvelte)
122+
const sourceInfos = options.globalCaches
123+
? cache.sourceInfos
124+
: new Map<ts.SourceFile, SourceInfo>()
125+
const host = createCompilerHost(
126+
cache,
127+
config.options,
128+
options,
129+
hasSvelte,
130+
sourceInfos
131+
)
122132
const rootNames = hasSvelte
123133
? [
124134
...config.fileNames,
@@ -132,7 +142,7 @@ export class ProjectIndexer {
132142
this.checker = this.program.getTypeChecker()
133143
this.packages = new Packages(options.projectRoot)
134144
this.indexedFiles = cache.indexedFiles
135-
this.sourceInfos = cache.sourceInfos
145+
this.sourceInfos = sourceInfos
136146
}
137147
public index(): void {
138148
const startTimestamp = Date.now()
@@ -261,13 +271,38 @@ export function languageForFileName(fileName: string): string | undefined {
261271
return undefined
262272
}
263273

264-
function deduplicateOccurrences(document: scip.scip.Document): void {
274+
export function deduplicateOccurrences(document: scip.scip.Document): void {
265275
const occurrences = new Map<string, scip.scip.Occurrence>()
266276
for (const occurrence of document.occurrences) {
267277
const key = `${occurrence.range.join(':')} ${occurrence.symbol}`
268278
const existing = occurrences.get(key)
269279
if (existing) {
270-
existing.symbol_roles |= occurrence.symbol_roles
280+
const symbolRoles = existing.symbol_roles | occurrence.symbol_roles
281+
const existingIsDefinition =
282+
(existing.symbol_roles & scip.scip.SymbolRole.Definition) !== 0
283+
const occurrenceIsDefinition =
284+
(occurrence.symbol_roles & scip.scip.SymbolRole.Definition) !== 0
285+
// svelte2tsx can map a generated reference and definition to the same
286+
// source range. Keep the definition as the survivor because it carries
287+
// the enclosing range and diagnostics associated with the declaration.
288+
if (occurrenceIsDefinition && !existingIsDefinition) {
289+
occurrence.symbol_roles = symbolRoles
290+
if (occurrence.enclosing_range.length === 0) {
291+
occurrence.enclosing_range = existing.enclosing_range
292+
}
293+
if (occurrence.diagnostics.length === 0) {
294+
occurrence.diagnostics = existing.diagnostics
295+
}
296+
occurrences.set(key, occurrence)
297+
} else {
298+
existing.symbol_roles = symbolRoles
299+
if (existing.enclosing_range.length === 0) {
300+
existing.enclosing_range = occurrence.enclosing_range
301+
}
302+
if (existing.diagnostics.length === 0) {
303+
existing.diagnostics = occurrence.diagnostics
304+
}
305+
}
271306
} else {
272307
occurrences.set(key, occurrence)
273308
}

src/Svelte.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import * as ts from 'typescript'
77
import { test } from 'uvu'
88
import * as assert from 'uvu/assert'
99

10+
import { GlobalCache, ProjectOptions } from './CommandLineOptions'
11+
import { ProjectIndexer } from './ProjectIndexer'
12+
import * as scip from './scip'
1013
import { SourceInfo } from './SourceInfo'
1114
import { SvelteSupport } from './Svelte'
1215

@@ -126,4 +129,51 @@ test('Svelte host preserves modern module resolution and rune modules', () => {
126129
}
127130
})
128131

132+
test('no-global-caches keeps Svelte source information project-local', () => {
133+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'scip-svelte-'))
134+
try {
135+
const fileName = path.join(directory, 'Component.svelte')
136+
fs.writeFileSync(fileName, '<h1>Hello</h1>\n')
137+
const cache: GlobalCache = {
138+
sources: new Map(),
139+
parsedCommandLines: new Map(),
140+
indexedFiles: new Set(),
141+
sourceInfos: new Map(),
142+
}
143+
const documents: scip.scip.Document[] = []
144+
const options: ProjectOptions = {
145+
cwd: directory,
146+
projectRoot: directory,
147+
projectDisplayName: directory,
148+
output: path.join(directory, 'index.scip'),
149+
inferTsconfig: false,
150+
progressBar: false,
151+
yarnWorkspaces: false,
152+
yarnBerryWorkspaces: false,
153+
pnpmWorkspaces: false,
154+
globalCaches: false,
155+
indexedProjects: new Set(),
156+
writeIndex: index => documents.push(...index.documents),
157+
}
158+
const config: ts.ParsedCommandLine = {
159+
options: {
160+
allowJs: true,
161+
allowNonTsExtensions: true,
162+
module: ts.ModuleKind.ESNext,
163+
moduleResolution: ts.ModuleResolutionKind.Bundler,
164+
},
165+
fileNames: [fileName],
166+
errors: [],
167+
}
168+
169+
new ProjectIndexer(config, options, cache).index()
170+
171+
assert.is(documents.length, 1)
172+
assert.is(documents[0].language, 'Svelte')
173+
assert.is(cache.sourceInfos.size, 0)
174+
} finally {
175+
fs.rmSync(directory, { recursive: true, force: true })
176+
}
177+
})
178+
129179
test.run()

src/Svelte.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,16 @@ class SvelteSourceInfo implements SourceInfo {
232232
return undefined
233233
}
234234
const lastCharacter = this.originalPosition(generatedEnd - 1)
235+
const startLine = this.lines[start.line]
236+
const lastLine = lastCharacter && this.lines[lastCharacter.line]
235237
if (
236238
!lastCharacter ||
239+
startLine === undefined ||
240+
lastLine === undefined ||
241+
start.column < 0 ||
242+
start.column > startLine.length ||
243+
lastCharacter.column < 0 ||
244+
lastCharacter.column >= lastLine.length ||
237245
start.source !== lastCharacter.source ||
238246
lastCharacter.line < start.line ||
239247
(lastCharacter.line === start.line && lastCharacter.column < start.column)
@@ -335,13 +343,15 @@ class SvelteSourceInfo implements SourceInfo {
335343
line: number,
336344
column: number
337345
): number[] | undefined {
346+
const lineText = this.lines[line]
347+
if (lineText === undefined || column < 0 || column > lineText.length) {
348+
return undefined
349+
}
338350
const candidates = ts.isStringLiteralLike(node)
339351
? [node.text, node.getText()]
340352
: [node.getText()]
341353
for (const candidate of candidates) {
342-
if (
343-
this.lines[line].slice(column, column + candidate.length) === candidate
344-
) {
354+
if (lineText.slice(column, column + candidate.length) === candidate) {
345355
return [line, column, column + candidate.length]
346356
}
347357
}

0 commit comments

Comments
 (0)