cspell
Version:
A Spelling Checker for Code!
214 lines • 7.28 kB
JavaScript
import assert from 'node:assert';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import { dirname, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath } from 'node:path';
import { readFileInfo } from '../../util/fileHelper.js';
import { createFromFile, normalizePath } from './fileEntryCache.js';
import { ShallowObjectCollection } from './ObjectCollection.js';
const cacheDataKeys = {
v: 'v',
r: 'r',
d: 'd',
};
/**
* Meta Data Version is used to detect if the structure of the meta data has changed.
* This is used in combination with the Suffix and the version of CSpell.
*/
const META_DATA_BASE_VERSION = '1';
const META_DATA_VERSION_SUFFIX = '-' + META_DATA_BASE_VERSION + '-' + Object.keys(cacheDataKeys).join('|');
/**
* Caches cspell results on disk
*/
export class DiskCache {
useCheckSum;
cspellVersion;
useUniversalCache;
cacheFileLocation;
cacheDir;
fileEntryCache;
dependencyCache = new Map();
dependencyCacheTree = {};
objectCollection = new ShallowObjectCollection();
ocCacheFileResult = new ShallowObjectCollection();
version;
constructor(cacheFileLocation, useCheckSum, cspellVersion, useUniversalCache) {
this.useCheckSum = useCheckSum;
this.cspellVersion = cspellVersion;
this.useUniversalCache = useUniversalCache;
this.cacheFileLocation = resolvePath(cacheFileLocation);
this.cacheDir = dirname(this.cacheFileLocation);
this.fileEntryCache = createFromFile(this.cacheFileLocation, useCheckSum, useUniversalCache);
this.version = calcVersion(cspellVersion);
}
async getCachedLintResults(filename) {
filename = normalizePath(filename);
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filename);
const meta = fileDescriptor.meta;
const data = meta?.data;
const result = data?.r;
const versionMatches = this.version === data?.v;
// Cached lint results are valid if and only if:
// 1. The file is present in the filesystem
// 2. The file has not changed since the time it was previously linted
// 3. The CSpell configuration has not changed since the time the file was previously linted
// If any of these are not true, we will not reuse the lint results.
if (fileDescriptor.notFound ||
fileDescriptor.changed ||
!meta ||
!result ||
!versionMatches ||
!this.checkDependencies(data.d)) {
return undefined;
}
const dd = { ...data };
if (dd.d) {
dd.d = setTreeEntry(this.dependencyCacheTree, dd.d);
}
dd.r = dd.r && this.normalizeResult(dd.r);
meta.data = this.objectCollection.get(dd);
// Skip reading empty files and files without lint error
const hasErrors = !!result && (result.errors > 0 || result.configErrors > 0 || result.issues.length > 0);
const cached = true;
const shouldReadFile = cached && hasErrors;
return {
...result,
elapsedTimeMs: undefined,
fileInfo: shouldReadFile ? await readFileInfo(filename) : { filename },
cached,
};
}
setCachedLintResults({ fileInfo, elapsedTimeMs: _, cached: __, ...result }, dependsUponFiles) {
const fileDescriptor = this.fileEntryCache.getFileDescriptor(fileInfo.filename);
const meta = fileDescriptor.meta;
if (fileDescriptor.notFound || !meta) {
return;
}
const data = this.objectCollection.get({
v: this.version,
r: this.normalizeResult(result),
d: this.calcDependencyHashes(dependsUponFiles),
});
meta.data = data;
}
reconcile() {
this.fileEntryCache.reconcile();
}
reset() {
this.fileEntryCache.destroy();
this.dependencyCache.clear();
this.dependencyCacheTree = {};
this.objectCollection = new ShallowObjectCollection();
this.ocCacheFileResult = new ShallowObjectCollection();
}
normalizeResult(result) {
const { issues, processed, errors, configErrors, ...rest } = result;
if (!Object.keys(rest).length) {
return this.ocCacheFileResult.get(result);
}
return this.ocCacheFileResult.get({ issues, processed, errors, configErrors });
}
calcDependencyHashes(dependsUponFiles) {
dependsUponFiles.sort();
const c = getTreeEntry(this.dependencyCacheTree, dependsUponFiles);
if (c?.d) {
return c.d;
}
const dependencies = dependsUponFiles.map((f) => this.getDependency(f));
return setTreeEntry(this.dependencyCacheTree, dependencies);
}
checkDependency(dep) {
const depFile = this.resolveFile(dep.f);
const cDep = this.dependencyCache.get(depFile);
if (cDep && compDep(dep, cDep))
return true;
if (cDep)
return false;
const d = this.getFileDep(depFile);
if (compDep(dep, d)) {
this.dependencyCache.set(depFile, dep);
return true;
}
this.dependencyCache.set(depFile, d);
return false;
}
getDependency(file) {
const dep = this.dependencyCache.get(file);
if (dep)
return dep;
const d = this.getFileDep(file);
this.dependencyCache.set(file, d);
return d;
}
getFileDep(file) {
assert(isAbsolutePath(file), `Dependency must be absolute "${file}"`);
const f = this.toRelFile(file);
let h;
try {
const buffer = fs.readFileSync(file);
h = this.getHash(buffer);
}
catch {
return { f };
}
return { f, h };
}
checkDependencies(dependencies) {
if (!dependencies)
return false;
for (const dep of dependencies) {
if (!this.checkDependency(dep)) {
return false;
}
}
return true;
}
getHash(buffer) {
return crypto.createHash('md5').update(buffer).digest('hex');
}
resolveFile(file) {
return normalizePath(resolvePath(this.cacheDir, file));
}
toRelFile(file) {
return normalizePath(this.useUniversalCache ? relativePath(this.cacheDir, file) : file);
}
}
function getTreeEntry(tree, keys) {
let r = tree;
for (const k of keys) {
r = r.c?.get(k);
if (!r)
return r;
}
return r;
}
function setTreeEntry(tree, deps, update = false) {
let r = tree;
for (const d of deps) {
const k = d.f;
if (!r.c) {
r.c = new Map();
}
const cn = r.c.get(k);
const n = cn ?? {};
if (!cn) {
r.c.set(k, n);
}
r = n;
}
let d = r.d;
if (!d || (r.d && update)) {
r.d = deps;
d = deps;
}
return d;
}
function compDep(a, b) {
return a.f === b.f && a.h === b.h;
}
function calcVersion(version) {
return version + META_DATA_VERSION_SUFFIX;
}
export const __testing__ = {
calcVersion,
};
//# sourceMappingURL=DiskCache.js.map