diff --git a/packages/cspell/src/app.ts b/packages/cspell/src/app.ts index f00ceaf64a9..84e395fe741 100644 --- a/packages/cspell/src/app.ts +++ b/packages/cspell/src/app.ts @@ -1,6 +1,5 @@ import type { Command } from 'commander'; import { program } from 'commander'; -import * as path from 'path'; import { satisfies as semverSatisfies } from 'semver'; import { commandCheck } from './commandCheck'; import { commandLink } from './commandLink'; @@ -9,7 +8,7 @@ import { commandSuggestion } from './commandSuggestion'; import { commandTrace } from './commandTrace'; import { ApplicationError } from './util/errors'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const npmPackage = require(path.join(__dirname, '..', 'package.json')); +const npmPackage = require('../package.json'); export { LinterCliOptions as Options } from './options'; export { CheckFailed } from './util/errors'; diff --git a/packages/cspell/src/lint/lint.ts b/packages/cspell/src/lint/lint.ts index c5720706290..37f2dd3e960 100644 --- a/packages/cspell/src/lint/lint.ts +++ b/packages/cspell/src/lint/lint.ts @@ -28,6 +28,9 @@ import { getTimeMeasurer } from '../util/timer'; import * as util from '../util/util'; import { LintRequest } from './LintRequest'; import chalk = require('chalk'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const npmPackage = require('../../package.json'); +const version = npmPackage.version; export async function runLint(cfg: LintRequest): Promise { let { reporter } = cfg; @@ -253,7 +256,7 @@ export async function runLint(cfg: LintRequest): Promise { const { root } = cfg; try { - const cacheSettings = await calcCacheSettings(configInfo.config, cfg.options, root); + const cacheSettings = await calcCacheSettings(configInfo.config, { ...cfg.options, version }, root); const files = await determineFilesToCheck(configInfo, cfg, reporter, globInfo); return await processFiles(files, configInfo, cacheSettings); diff --git a/packages/cspell/src/options.ts b/packages/cspell/src/options.ts index 3f9c5440263..889bb3512cf 100644 --- a/packages/cspell/src/options.ts +++ b/packages/cspell/src/options.ts @@ -1,6 +1,6 @@ import type { CacheOptions } from './util/cache'; -export interface LinterOptions extends BaseOptions, CacheOptions { +export interface LinterOptions extends BaseOptions, Omit { /** * Display verbose information */ diff --git a/packages/cspell/src/util/cache/CacheOptions.ts b/packages/cspell/src/util/cache/CacheOptions.ts index faeac8bd286..fc8b5c8899b 100644 --- a/packages/cspell/src/util/cache/CacheOptions.ts +++ b/packages/cspell/src/util/cache/CacheOptions.ts @@ -1,6 +1,11 @@ import type { CacheStrategy } from '@cspell/cspell-types'; export interface CacheOptions { + /** + * The version of `cspell` that made the cache entry. + * Cache entries must match the `major.minor` version. + */ + version: string; /** * Store the info about processed files in order to only operate on the changed ones. */ diff --git a/packages/cspell/src/util/cache/DiskCache.test.ts b/packages/cspell/src/util/cache/DiskCache.test.ts index 20b7871ad4f..293edc55f01 100644 --- a/packages/cspell/src/util/cache/DiskCache.test.ts +++ b/packages/cspell/src/util/cache/DiskCache.test.ts @@ -39,7 +39,7 @@ describe('DiskCache', () => { }; beforeEach(() => { - diskCache = new DiskCache('.foobar', false); + diskCache = new DiskCache('.foobar', false, 'version'); fileEntryCache = mockCreateFileEntryCache.mock.results[0].value; }); @@ -173,6 +173,7 @@ function entry(result: CachedFileResult, dependencies: string[] = [], size = 100 data: { r: result, d: dependencies, + v: 'version', }, }, }; diff --git a/packages/cspell/src/util/cache/DiskCache.ts b/packages/cspell/src/util/cache/DiskCache.ts index e19965712c5..99cbe46bb64 100644 --- a/packages/cspell/src/util/cache/DiskCache.ts +++ b/packages/cspell/src/util/cache/DiskCache.ts @@ -16,6 +16,8 @@ interface CachedData { r: CachedFileResult; /** dependencies */ d: string[]; + /** meta version */ + v: string; } interface CSpellCachedMetaData { @@ -32,7 +34,7 @@ export class DiskCache implements CSpellLintResultCache { private changedDependencies: Set = new Set(); private knownDependencies: Set = new Set(); - constructor(cacheFileLocation: string, useCheckSum: boolean) { + constructor(cacheFileLocation: string, useCheckSum: boolean, readonly version: string) { this.fileEntryCache = fileEntryCache.createFromFile(resolvePath(cacheFileLocation), useCheckSum); } @@ -41,13 +43,21 @@ export class DiskCache implements CSpellLintResultCache { const meta = fileDescriptor.meta as CSpellCacheMeta; 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 || !this.checkDependencies(data.d)) { + if ( + fileDescriptor.notFound || + fileDescriptor.changed || + !meta || + !result || + !versionMatches || + !this.checkDependencies(data.d) + ) { return undefined; } @@ -77,6 +87,7 @@ export class DiskCache implements CSpellLintResultCache { const data: CachedData = { r: result, d: dependsUponFiles, + v: this.version, }; meta.data = data; diff --git a/packages/cspell/src/util/cache/createCache.test.ts b/packages/cspell/src/util/cache/createCache.test.ts index 1135cba8750..ec0ed2f39b8 100644 --- a/packages/cspell/src/util/cache/createCache.test.ts +++ b/packages/cspell/src/util/cache/createCache.test.ts @@ -1,15 +1,17 @@ -import { calcCacheSettings, createCache } from './createCache'; -import { DiskCache } from './DiskCache'; -import { CacheOptions } from './CacheOptions'; import * as path from 'path'; import { resolve as r } from 'path'; import { CreateCacheSettings } from '.'; +import { CacheOptions } from './CacheOptions'; +import { calcCacheSettings, createCache, __testing__ } from './createCache'; +import { DiskCache } from './DiskCache'; import { DummyCache } from './DummyCache'; jest.mock('./DiskCache'); const mockedDiskCache = jest.mocked(DiskCache); +const version = '5.20.0-alpha.192'; + const U = undefined; const T = true; const F = false; @@ -30,8 +32,11 @@ describe('Validate calcCacheSettings', () => { ${{}} | ${{ cacheStrategy: 'content' }} | ${'.'} | ${cco(F, U, 'content')} | ${'override default strategy'} ${{}} | ${co(T, U, 'content')} | ${'.'} | ${cco(T, U, 'content')} | ${'override strategy'} ${cs(U, U, 'content')} | ${co(T, U, 'metadata')} | ${'.'} | ${cco(T, U, 'metadata')} | ${'override config strategy'} - ${cs(T, U, 'content')} | ${{}} | ${'.'} | ${cco(T, U, 'content')} | ${'override default strategy'} + ${cs(T, U, 'content')} | ${{ version }} | ${'.'} | ${cco(T, U, 'content')} | ${'override default strategy'} `('calcCacheSettings $comment - $config $options $root', async ({ config, options, root, expected }) => { + if (!options.version) { + options.version = version; + } expect(await calcCacheSettings({ cache: config }, options, root)).toEqual(expected); }); }); @@ -52,6 +57,26 @@ describe('Validate createCache', () => { }); }); +describe('validate normalizeVersion', () => { + test.each` + version | expected + ${'5.8'} | ${'5.8'} + ${'5.8.30'} | ${'5.8'} + ${'5.8.30-alpha.2'} | ${'5.8'} + ${'5.28.30-alpha.2'} | ${'5.28'} + ${'6.0.30-alpha.2'} | ${'6.0'} + `('normalizeVersion $version', ({ version, expected }) => { + expect(__testing__.normalizeVersion(version)).toBe(expected + __testing__.versionSuffix); + }); + test.each` + version + ${'5'} + ${''} + `('normalizeVersion bad "$version"', ({ version }) => { + expect(() => __testing__.normalizeVersion(version)).toThrow(); + }); +}); + /** * CreateCacheSettings */ @@ -63,13 +88,13 @@ function cco( if (cacheLocation) { cacheLocation = path.resolve(process.cwd(), cacheLocation); } - return { useCache, cacheLocation, cacheStrategy }; + return { useCache, cacheLocation, cacheStrategy, version }; } function cs(useCache?: boolean, cacheLocation?: string, cacheStrategy?: CacheOptions['cacheStrategy']) { - return { useCache, cacheLocation, cacheStrategy }; + return { useCache, cacheLocation, cacheStrategy, version }; } function co(cache?: boolean, cacheLocation?: string, cacheStrategy?: CacheOptions['cacheStrategy']) { - return { cache, cacheLocation, cacheStrategy }; + return { cache, cacheLocation, cacheStrategy, version }; } diff --git a/packages/cspell/src/util/cache/createCache.ts b/packages/cspell/src/util/cache/createCache.ts index c898bfcc617..f3a8eba0cc5 100644 --- a/packages/cspell/src/util/cache/createCache.ts +++ b/packages/cspell/src/util/cache/createCache.ts @@ -1,24 +1,33 @@ import { CacheSettings, CSpellSettings } from '@cspell/cspell-types'; +import assert from 'assert'; +import { stat } from 'fs-extra'; import path from 'path'; import { CacheOptions } from '.'; +import { isError } from '../errors'; import { CSpellLintResultCache } from './CSpellLintResultCache'; import { DiskCache } from './DiskCache'; import { DummyCache } from './DummyCache'; -import { stat } from 'fs-extra'; -import { isError } from '../errors'; // cspell:word cspellcache export const DEFAULT_CACHE_LOCATION = '.cspellcache'; -export type CreateCacheSettings = Required; +export interface CreateCacheSettings extends Required { + /** + * cspell version used to validate cache entries. + */ + version: string; +} + +const versionSuffix = ''; /** * Creates CSpellLintResultCache (disk cache if caching is enabled in config or dummy otherwise) */ - export function createCache(options: CreateCacheSettings): CSpellLintResultCache { const { useCache, cacheLocation, cacheStrategy } = options; - return useCache ? new DiskCache(path.resolve(cacheLocation), cacheStrategy === 'content') : new DummyCache(); + return useCache + ? new DiskCache(path.resolve(cacheLocation), cacheStrategy === 'content', normalizeVersion(options.version)) + : new DummyCache(); } export async function calcCacheSettings( @@ -37,6 +46,7 @@ export async function calcCacheSettings( useCache, cacheLocation, cacheStrategy, + version: cacheOptions.version, }; } @@ -52,3 +62,18 @@ async function resolveCacheLocation(cacheLocation: string): Promise { throw err; } } + +/** + * Normalizes the version and return only `major.minor + versionSuffix` + * @param version The cspell semantic version. + */ +function normalizeVersion(version: string): string { + const parts = version.split('.').slice(0, 2); + assert(parts.length === 2); + return parts.join('.') + versionSuffix; +} + +export const __testing__ = { + normalizeVersion, + versionSuffix, +};