Skip to content

Commit

Permalink
fix: eslint-plugin improve performance (#2616)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Mar 23, 2022
1 parent 8d028ea commit b1a9bed
Show file tree
Hide file tree
Showing 24 changed files with 514 additions and 136 deletions.
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

0 comments on commit b1a9bed

Please sign in to comment.