Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: eslint-plugin improve performance #2616

Merged
merged 15 commits into from Mar 23, 2022
2 changes: 1 addition & 1 deletion jest.config.js
Expand Up @@ -3,7 +3,7 @@ module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec|perf))\\.[jt]sx?$',
testRegex: '(/__tests__/.*|\\.(test|spec|perf))\\.[jt]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coverageReporters: ['html', 'json', ['lcov', { projectRoot: __dirname }], 'text'],
// "coverageProvider": "v8",
Expand Down
42 changes: 39 additions & 3 deletions packages/cspell-eslint-plugin/src/cspell-eslint-plugin.ts
@@ -1,7 +1,7 @@
// cspell:ignore TSESTree
import type { TSESTree } from '@typescript-eslint/types';
import assert from 'assert';
import { createTextDocument, CSpellSettings, DocumentValidator, ValidationIssue } from 'cspell-lib';
import { createTextDocument, CSpellSettings, DocumentValidator, ValidationIssue, TextDocument } from 'cspell-lib';
import type { Rule } from 'eslint';
// eslint-disable-next-line node/no-missing-import
import type { Comment, Identifier, Literal, Node, TemplateElement, ImportSpecifier } from 'estree';
Expand Down Expand Up @@ -60,8 +60,7 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
const importedIdentifiers = new Set<string>();
isDebugMode = options.debugMode || false;
isDebugMode && logContext(context);
const doc = createTextDocument({ uri: context.getFilename(), content: context.getSourceCode().getText() });
const validator = new DocumentValidator(doc, options, defaultSettings);
const validator = getDocValidator(context);
validator.prepareSync();

function checkLiteral(node: Literal & Rule.NodeParentExtension) {
Expand Down Expand Up @@ -369,3 +368,40 @@ export const configs = {
},
},
};

interface CachedDoc {
filename: string;
doc: TextDocument;
}

const cache: { lastDoc: CachedDoc | undefined } = { lastDoc: undefined };

const docValCache = new WeakMap<TextDocument, DocumentValidator>();

function getDocValidator(context: Rule.RuleContext): DocumentValidator {
const text = context.getSourceCode().getText();
const doc = getTextDocument(context.getFilename(), text);
const cachedValidator = docValCache.get(doc);
if (cachedValidator) {
cachedValidator.updateDocumentText(text);
return cachedValidator;
}

const options = normalizeOptions(context.options[0]);
isDebugMode = options.debugMode || false;
isDebugMode && logContext(context);
const validator = new DocumentValidator(doc, options, defaultSettings);
docValCache.set(doc, validator);
return validator;
}

function getTextDocument(filename: string, content: string): TextDocument {
if (cache.lastDoc?.filename === filename) {
return cache.lastDoc.doc;
}

const doc = createTextDocument({ uri: filename, content });
// console.error(`CreateTextDocument: ${doc.uri}`);
cache.lastDoc = { filename, doc };
return doc;
}
18 changes: 16 additions & 2 deletions packages/cspell-lib/api/api.d.ts
Expand Up @@ -376,6 +376,10 @@ interface Position {
line: number;
character: number;
}
/**
* Range offset tuple.
*/
declare type SimpleRange$1 = [start: number, end: number];
interface TextDocumentLine {
readonly text: string;
readonly offset: number;
Expand Down Expand Up @@ -420,7 +424,12 @@ interface CreateTextDocumentParams {
locale?: string | undefined;
version?: number | undefined;
}
interface TextDocumentContentChangeEvent {
range?: SimpleRange$1;
text: string;
}
declare function createTextDocument({ uri, content, languageId, locale, version, }: CreateTextDocumentParams): TextDocument;
declare function updateTextDocument(doc: TextDocument, edits: TextDocumentContentChangeEvent[], version?: number): TextDocument;

/**
* Handles loading of `.pnp.js` and `.pnp.js` files.
Expand Down Expand Up @@ -469,7 +478,7 @@ declare type CSpellSettingsWSTO = OptionalOrUndefined<CSpellSettingsWithSourceTr
declare type CSpellSettingsI = CSpellSettingsInternal;
declare const currentSettingsFileVersion = "0.2";
declare const ENV_CSPELL_GLOB_ROOT = "CSPELL_GLOB_ROOT";
declare function mergeSettings(left: CSpellSettingsWSTO | CSpellSettingsI, ...settings: (CSpellSettingsWSTO | CSpellSettingsI)[]): CSpellSettingsI;
declare function mergeSettings(left: CSpellSettingsWSTO | CSpellSettingsI, ...settings: (CSpellSettingsWSTO | CSpellSettingsI | undefined)[]): CSpellSettingsI;
declare function mergeInDocSettings(left: CSpellSettingsWSTO, right: CSpellSettingsWSTO): CSpellSettingsWST;
declare function calcOverrideSettings(settings: CSpellSettingsWSTO, filename: string): CSpellSettingsI;
/**
Expand Down Expand Up @@ -571,6 +580,7 @@ declare class DocumentValidator {
private _prepared;
private _preparations;
private _preparationTime;
private _suggestions;
/**
* @param doc - Document to validate
* @param config - configuration to use (not finalized).
Expand All @@ -580,15 +590,19 @@ declare class DocumentValidator {
prepareSync(): void;
prepare(): Promise<void>;
private _prepareAsync;
private _updatePrep;
/**
* The amount of time in ms to prepare for validation.
*/
get prepTime(): number;
checkText(range: SimpleRange, _text: string, _scope: string[]): ValidationIssue[];
get document(): TextDocument;
updateDocumentText(text: string): void;
private addPossibleError;
private catchError;
private errorCatcherWrapper;
private suggest;
private genSuggestions;
}
declare type Offset = number;
declare type SimpleRange = readonly [Offset, Offset];
Expand Down Expand Up @@ -783,4 +797,4 @@ declare function resolveFile(filename: string, relativeTo: string): ResolveFileR
declare function clearCachedFiles(): Promise<void>;
declare function getDictionary(settings: CSpellUserSettings): Promise<SpellingDictionaryCollection>;

export { CheckTextInfo, ConfigurationDependencies, CreateTextDocumentParams, DetermineFinalDocumentSettingsResult, Document, DocumentValidator, DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, ExcludeFilesGlobMap, ExclusionFunction, exclusionHelper_d as ExclusionHelper, ImportError, ImportFileRefWithError, IncludeExcludeFlag, IncludeExcludeOptions, index_link_d as Link, Logger, SpellCheckFileOptions, SpellCheckFileResult, SpellingDictionary, SpellingDictionaryCollection, SpellingDictionaryLoadError, SuggestOptions, SuggestedWord, SuggestionError, SuggestionOptions, SuggestionsForWordResult, text_d as Text, TextDocument, TextDocumentLine, TextInfoItem, TraceOptions, TraceResult, ValidationIssue, calcOverrideSettings, checkFilenameMatchesGlob, checkText, clearCachedFiles, clearCachedSettingsFiles, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createSpellingDictionary, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, finalizeSettings, getCachedFileSize, getDefaultSettings, getDictionary, getGlobalSettings, getLanguagesForExt, getLogger, getSources, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, loadPnPSync, mergeInDocSettings, mergeSettings, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveFile, searchForConfig, sectionCSpell, setLogger, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, validateText };
export { CheckTextInfo, ConfigurationDependencies, CreateTextDocumentParams, DetermineFinalDocumentSettingsResult, Document, DocumentValidator, DocumentValidatorOptions, ENV_CSPELL_GLOB_ROOT, ExcludeFilesGlobMap, ExclusionFunction, exclusionHelper_d as ExclusionHelper, ImportError, ImportFileRefWithError, IncludeExcludeFlag, IncludeExcludeOptions, index_link_d as Link, Logger, SpellCheckFileOptions, SpellCheckFileResult, SpellingDictionary, SpellingDictionaryCollection, SpellingDictionaryLoadError, SuggestOptions, SuggestedWord, SuggestionError, SuggestionOptions, SuggestionsForWordResult, text_d as Text, TextDocument, TextDocumentLine, TextInfoItem, TraceOptions, TraceResult, ValidationIssue, calcOverrideSettings, checkFilenameMatchesGlob, checkText, clearCachedFiles, clearCachedSettingsFiles, combineTextAndLanguageSettings, combineTextAndLanguageSettings as constructSettingsForText, createSpellingDictionary, createTextDocument, currentSettingsFileVersion, defaultConfigFilenames, defaultFileName, defaultFileName as defaultSettingsFilename, determineFinalDocumentSettings, extractDependencies, extractImportErrors, fileToDocument, finalizeSettings, getCachedFileSize, getDefaultSettings, getDictionary, getGlobalSettings, getLanguagesForExt, getLogger, getSources, isBinaryFile, isSpellingDictionaryLoadError, loadConfig, loadPnP, loadPnPSync, mergeInDocSettings, mergeSettings, readRawSettings, readSettings, readSettingsFiles, refreshDictionaryCache, resolveFile, searchForConfig, sectionCSpell, setLogger, spellCheckDocument, spellCheckFile, suggestionsForWord, suggestionsForWords, traceWords, traceWordsAsync, updateTextDocument, validateText };
30 changes: 29 additions & 1 deletion packages/cspell-lib/src/Models/TextDocument.test.ts
@@ -1,4 +1,4 @@
import { createTextDocument } from './TextDocument';
import { createTextDocument, updateTextDocument } from './TextDocument';
import { Uri } from './Uri';

describe('TextDocument', () => {
Expand All @@ -10,4 +10,32 @@ describe('TextDocument', () => {
expect(doc.uri).toBeInstanceOf(Uri);
expect(doc.uri.toString()).toEqual(Uri.file(__filename).toString());
});

test('update', () => {
const doc = sampleDoc();
expect(doc.version).toBe(1);
const t = 'self';
const textCopy = doc.text;
const offset = doc.text.indexOf(t);
updateTextDocument(doc, [{ range: [offset, offset + t.length], text: 'showSelf' }]);
expect(doc.version).toBe(2);
expect(doc.text).not.toEqual(textCopy);
expect(doc.text.startsWith('showSelf', offset)).toBe(true);
updateTextDocument(doc, [{ text: textCopy }]);
expect(doc.text).toBe(textCopy);
});
});

function sampleDoc(filename?: string, content?: string) {
filename = filename ?? __filename;
content =
content ??
`
import * as fs from 'fs';

export function self(): string {
return fs.readFileSync(__filename, 'utf8');
}
`;
return createTextDocument({ uri: filename, content });
}
53 changes: 51 additions & 2 deletions packages/cspell-lib/src/Models/TextDocument.ts
@@ -1,6 +1,7 @@
import { getLanguagesForBasename } from '../LanguageIds';
import * as Uri from './Uri';
import { TextDocument as VsTextDocument } from 'vscode-languageserver-textdocument';
import assert from 'assert';

export type DocumentUri = Uri.Uri;

Expand All @@ -9,6 +10,11 @@ export interface Position {
character: number;
}

/**
* Range offset tuple.
*/
export type SimpleRange = [start: number, end: number];

export interface TextDocumentLine {
readonly text: string;
readonly offset: number;
Expand Down Expand Up @@ -55,15 +61,23 @@ class TextDocumentImpl implements TextDocument {

constructor(
readonly uri: DocumentUri,
readonly text: string,
text: string,
readonly languageId: string | string[],
readonly locale: string | undefined,
readonly version: number
version: number
) {
const primaryLanguageId = typeof languageId === 'string' ? languageId : languageId[0] || 'plaintext';
this.vsTextDoc = VsTextDocument.create(uri.toString(), primaryLanguageId, version, text);
}

get version(): number {
return this.vsTextDoc.version;
}

get text(): string {
return this.vsTextDoc.getText();
}

positionAt(offset: number): Position {
return this.vsTextDoc.positionAt(offset);
}
Expand All @@ -90,6 +104,27 @@ class TextDocumentImpl implements TextDocument {
position,
};
}

/**
* Apply edits to the text.
* Note: the edits are applied one after the other.
* @param edits - changes to the text
* @param version - optional version to use.
* @returns this
*/
update(edits: TextDocumentContentChangeEvent[], version?: number): this {
version = version ?? this.version + 1;
for (const edit of edits) {
const vsEdit = edit.range
? {
range: { start: this.positionAt(edit.range[0]), end: this.positionAt(edit.range[1]) },
text: edit.text,
}
: edit;
VsTextDocument.update(this.vsTextDoc, [vsEdit], version);
}
return this;
}
}

export interface CreateTextDocumentParams {
Expand All @@ -100,6 +135,11 @@ export interface CreateTextDocumentParams {
version?: number | undefined;
}

export interface TextDocumentContentChangeEvent {
range?: SimpleRange;
text: string;
}

export function createTextDocument({
uri,
content,
Expand All @@ -113,3 +153,12 @@ export function createTextDocument({
languageId = languageId.length === 0 ? 'text' : languageId;
return new TextDocumentImpl(uri, content, languageId, locale, version);
}

export function updateTextDocument(
doc: TextDocument,
edits: TextDocumentContentChangeEvent[],
version?: number
): TextDocument {
assert(doc instanceof TextDocumentImpl, 'Unknown TextDocument type');
return doc.update(edits, version);
}
6 changes: 0 additions & 6 deletions packages/cspell-lib/src/Settings/CSpellSettingsServer.test.ts
Expand Up @@ -27,7 +27,6 @@ describe('Validate CSpellSettingsServer', () => {
name: 'Left|Right',
id: '|',
enabledLanguageIds: [],
languageSettings: [],
source: { name: 'Left|Right', sources: [left, right] },
})
);
Expand All @@ -42,7 +41,6 @@ describe('Validate CSpellSettingsServer', () => {
name: '|enabledName',
id: 'left|enabledId',
enabledLanguageIds: [],
languageSettings: [],
source: { name: 'left|enabledName', sources: [csi(left), csi(enabled)] },
})
);
Expand All @@ -59,7 +57,6 @@ describe('Validate CSpellSettingsServer', () => {
name: '|',
id: [left.id, right.id].join('|'),
enabledLanguageIds: [],
languageSettings: [],
source: { name: 'left|right', sources: [left, right] },
})
);
Expand All @@ -74,7 +71,6 @@ describe('Validate CSpellSettingsServer', () => {
name: '|',
id: '|',
enabledLanguageIds: [],
languageSettings: [],
source: { name: 'left|right', sources: [csi(a), csi(b)] },
})
);
Expand Down Expand Up @@ -106,7 +102,6 @@ describe('Validate CSpellSettingsServer', () => {
name: '|',
id: [left.id, right.id].join('|'),
enabledLanguageIds: [],
languageSettings: [],
files: left.files?.concat(right.files || []),
ignorePaths: left.ignorePaths?.concat(right.ignorePaths || []),
overrides: left.overrides?.concat(right.overrides || []),
Expand Down Expand Up @@ -144,7 +139,6 @@ describe('Validate CSpellSettingsServer', () => {
id: [left.id, right.id].join('|'),
version: right.version,
enabledLanguageIds: [],
languageSettings: [],
files: left.files?.concat(right.files || []),
ignorePaths: right.ignorePaths,
overrides: right.overrides,
Expand Down
26 changes: 10 additions & 16 deletions packages/cspell-lib/src/Settings/CSpellSettingsServer.ts
Expand Up @@ -3,7 +3,6 @@ import type {
CSpellUserSettings,
Glob,
ImportFileRef,
LanguageSetting,
Source,
} from '@cspell/cspell-types';
import { GlobMatcher } from 'cspell-glob';
Expand All @@ -28,6 +27,10 @@ export const configSettingsFileVersion0_2 = '0.2';
export const currentSettingsFileVersion = configSettingsFileVersion0_2;
export const ENV_CSPELL_GLOB_ROOT = 'CSPELL_GLOB_ROOT';

function _unique<T>(a: T[]): T[] {
return [...new Set(a)];
}

/**
* Merges two lists and removes duplicates. Order is NOT preserved.
*/
Expand All @@ -39,8 +42,9 @@ function mergeListUnique<T>(left: T[] | undefined, right: T[] | undefined): T[]
function mergeListUnique<T>(left: T[] | undefined, right: T[] | undefined): T[] | undefined {
if (left === undefined) return right;
if (right === undefined) return left;
const uniqueItems = new Set([...left, ...right]);
return [...uniqueItems.keys()];
if (!right.length) return left;
if (!left.length) return right;
return _unique([...left, ...right]);
}

/**
Expand Down Expand Up @@ -91,13 +95,6 @@ function mergeObjects<T>(left?: T, right?: T): T | undefined {
return { ...left, ...right };
}

function tagLanguageSettings(tag: string, settings: LanguageSetting[] = []): LanguageSetting[] {
return settings.map((s) => ({
id: tag + '.' + (s.id || s.locale || s.languageId),
...s,
}));
}

function replaceIfNotEmpty<T>(left: Array<T> = [], right: Array<T> = []) {
const filtered = right.filter((a) => !!a);
if (filtered.length) {
Expand All @@ -108,9 +105,9 @@ function replaceIfNotEmpty<T>(left: Array<T> = [], right: Array<T> = []) {

export function mergeSettings(
left: CSpellSettingsWSTO | CSpellSettingsI,
...settings: (CSpellSettingsWSTO | CSpellSettingsI)[]
...settings: (CSpellSettingsWSTO | CSpellSettingsI | undefined)[]
): CSpellSettingsI {
const rawSettings = settings.reduce<CSpellSettingsI>(merge, toInternalSettings(left));
const rawSettings = settings.filter((a) => !!a).reduce<CSpellSettingsI>(merge, toInternalSettings(left));
return util.clean(rawSettings);
}

Expand Down Expand Up @@ -166,10 +163,7 @@ function merge(
dictionaryDefinitions: mergeListUnique(_left.dictionaryDefinitions, _right.dictionaryDefinitions),
dictionaries: mergeListUnique(_left.dictionaries, _right.dictionaries),
noSuggestDictionaries: mergeListUnique(_left.noSuggestDictionaries, _right.noSuggestDictionaries),
languageSettings: mergeList(
tagLanguageSettings(leftId, _left.languageSettings),
tagLanguageSettings(rightId, _right.languageSettings)
),
languageSettings: mergeList(_left.languageSettings, _right.languageSettings),
enabled: _right.enabled !== undefined ? _right.enabled : _left.enabled,
files: mergeListUnique(_left.files, _right.files),
ignorePaths: versionBasedMergeList(_left.ignorePaths, _right.ignorePaths, version),
Expand Down