@anysphere/scip-typescript
Version:
SCIP indexer for TypeScript and JavaScript
217 lines (209 loc) • 6.77 kB
text/typescript
import * as path from 'path'
import prettyMilliseconds from 'pretty-ms'
import ProgressBar from 'progress'
import * as ts from 'typescript'
import { GlobalCache, ProjectOptions } from './CommandLineOptions'
import { FileIndexer } from './FileIndexer'
import { Input } from './Input'
import { Packages } from './Packages'
import * as scip from './scip'
import { ScipSymbol } from './ScipSymbol'
import { Ignore } from 'ignore'
function createCompilerHost(
cache: GlobalCache,
compilerOptions: ts.CompilerOptions,
projectOptions: ProjectOptions
): ts.CompilerHost {
const host = ts.createCompilerHost(compilerOptions)
if (!projectOptions.globalCaches) {
return host
}
const hostCopy = { ...host }
host.getParsedCommandLine = (fileName: string) => {
if (!hostCopy.getParsedCommandLine) {
return undefined
}
const fromCache = cache.parsedCommandLines.get(fileName)
if (fromCache !== undefined) {
return fromCache
}
const result = hostCopy.getParsedCommandLine(fileName)
if (result !== undefined) {
// Don't cache undefined results even if they could be cached
// theoretically. The big performance gains from this cache come from
// caching non-undefined results.
cache.parsedCommandLines.set(fileName, result)
}
return result
}
host.getSourceFile = (
fileName,
languageVersion,
onError,
shouldCreateNewSourceFile
) => {
const fromCache = cache.sources.get(fileName)
if (fromCache !== undefined) {
const [sourceFile, cachedLanguageVersion] = fromCache
if (isSameLanguageVersion(languageVersion, cachedLanguageVersion)) {
return sourceFile
}
}
const result = hostCopy.getSourceFile(
fileName,
languageVersion,
onError,
shouldCreateNewSourceFile
)
if (result !== undefined) {
// Don't cache undefined results even if they could be cached
// theoretically. The big performance gains from this cache come from
// caching non-undefined results.
cache.sources.set(fileName, [result, languageVersion])
}
return result
}
return host
}
export class ProjectIndexer {
private program: ts.Program
private checker: ts.TypeChecker
private symbolCache: Map<ts.Node, ScipSymbol> = new Map()
private hasConstructor: Map<ts.ClassDeclaration, boolean> = new Map()
private packages: Packages
constructor(
public readonly config: ts.ParsedCommandLine,
public readonly options: ProjectOptions,
public readonly ignoreMap: Map<string, Ignore>,
cache: GlobalCache
) {
const host = createCompilerHost(cache, config.options, options)
this.program = ts.createProgram(config.fileNames, config.options, host)
this.checker = this.program.getTypeChecker()
this.packages = new Packages(options.projectRoot)
}
public index(): void {
const startTimestamp = Date.now()
const sourceFiles = this.program.getSourceFiles()
const filesToIndex: ts.SourceFile[] = []
console.log(`+ cwd ${this.options.cwd}`)
// Visit every sourceFile in the program
for (const sourceFile of sourceFiles) {
const includes = this.config.fileNames.includes(sourceFile.fileName)
if (!includes) {
continue
}
filesToIndex.push(sourceFile)
}
if (filesToIndex.length === 0) {
throw new Error(
`no indexable files in project '${this.options.projectDisplayName}'`
)
}
const jobs: ProgressBar | undefined = this.options.progressBar
? new ProgressBar(
` ${this.options.projectDisplayName} [:bar] :current/:total :title`,
{
total: filesToIndex.length,
renderThrottle: 100,
incomplete: '_',
complete: '#',
width: 20,
clear: true,
stream: process.stderr,
}
)
: undefined
let lastWrite = startTimestamp
for (const [index, sourceFile] of filesToIndex.entries()) {
const title = path.relative(this.options.cwd, sourceFile.fileName)
// check gitignore
if (this.options.gitignore) {
let pIgnores = [];
for (const [key, value] of this.ignoreMap.entries()) {
if (sourceFile.fileName.startsWith(key)) {
pIgnores.push(value);
}
}
let doIgnore = false;
for (const ignore of pIgnores) {
if (ignore.ignores(title)) {
doIgnore = true;
break;
}
}
if (doIgnore) {
continue
}
}
jobs?.tick({ title })
if (!this.options.progressBar) {
const now = Date.now()
const elapsed = now - lastWrite
if (elapsed > 1000 && index > 2) {
lastWrite = now
process.stdout.write('.')
}
}
const document = new scip.scip.Document({
relative_path: path.relative(this.options.cwd, sourceFile.fileName),
occurrences: [],
})
const input = new Input(sourceFile.fileName, sourceFile.getText())
const visitor = new FileIndexer(
this.checker,
this.options,
input,
document,
this.symbolCache,
this.hasConstructor,
this.packages,
sourceFile
)
try {
visitor.index()
} catch (error) {
console.error(
`unexpected error indexing project root '${this.options.cwd}'`,
error
)
}
if (visitor.document.occurrences.length > 0) {
this.options.writeIndex(
new scip.scip.Index({
documents: [visitor.document],
})
)
}
}
jobs?.terminate()
const elapsed = Date.now() - startTimestamp
if (!this.options.progressBar && lastWrite > startTimestamp) {
process.stdout.write('\n')
}
console.log(
`+ ${this.options.projectDisplayName} (${prettyMilliseconds(elapsed)})`
)
}
}
function isSameLanguageVersion(
a: ts.ScriptTarget | ts.CreateSourceFileOptions,
b: ts.ScriptTarget | ts.CreateSourceFileOptions
): boolean {
if (typeof a === 'number' && typeof b === 'number') {
return a === b
}
if (typeof a === 'number' || typeof b === 'number') {
// Different shape: one is ts.ScriptTarget, the other is
// ts.CreateSourceFileOptions
return false
}
return (
a.languageVersion === b.languageVersion &&
a.impliedNodeFormat === b.impliedNodeFormat
// Ignore setExternalModuleIndicator even if that increases the risk of a
// false positive. A local experiment revealed that we never get a cache hit
// if we compare setExternalModuleIndicator since it's function with a
// unique reference on every `CompilerHost.getSourceFile` callback.
)
}