Skip to content

Commit

Permalink
fix: Detect when module default is used with cspell.config.js files. (
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Aug 15, 2021
1 parent b02f428 commit e05aeff
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 67 deletions.
1 change: 1 addition & 0 deletions packages/cspell-bundled-dicts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"clean": "echo clean",
"clean-build": "npm run clean && npm run build",
"prepare": "npm run clean-build",
"watch": "npm run build -- --watch",
"test": "node ../../bin.js \"*.{txt,md,ts}\""
},
"repository": {
Expand Down
65 changes: 53 additions & 12 deletions packages/cspell-lib/src/Settings/CSpellSettingsServer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import * as json from 'comment-json';
import {
RegExpPatternDefinition,
CSpellSettingsWithSourceTrace,
CSpellUserSettings,
Glob,
Source,
GlobDef,
ImportFileRef,
LanguageSetting,
Pattern,
CSpellSettingsWithSourceTrace,
ImportFileRef,
GlobDef,
PnPSettings,
RegExpPatternDefinition,
Source,
} from '@cspell/cspell-types';
import * as json from 'comment-json';
import { cosmiconfig, cosmiconfigSync, Options as CosmicOptions, OptionsSync as CosmicOptionsSync } from 'cosmiconfig';
import { GlobMatcher } from 'cspell-glob';
import * as path from 'path';
import { normalizePathForDictDefs } from './DictionarySettings';
import * as util from '../util/util';
import { URI } from 'vscode-uri';
import { logError, logWarning } from '../util/logger';
import { resolveFile } from '../util/resolveFile';
import * as util from '../util/util';
import { normalizePathForDictDefs } from './DictionarySettings';
import { getRawGlobalSettings } from './GlobalSettings';
import { cosmiconfig, cosmiconfigSync, OptionsSync as CosmicOptionsSync, Options as CosmicOptions } from 'cosmiconfig';
import { GlobMatcher } from 'cspell-glob';
import { ImportError } from './ImportError';
import { LoaderResult, pnpLoader } from './pnpLoader';
import { URI } from 'vscode-uri';

type CSpellSettingsVersion = Exclude<CSpellSettings['version'], undefined>;

const supportedCSpellConfigVersions: CSpellSettingsVersion[] = ['0.2'];
const configSettingsFileVersion0_1 = '0.1';
const configSettingsFileVersion0_2 = '0.2';
const currentSettingsFileVersion = configSettingsFileVersion0_2;
Expand Down Expand Up @@ -104,8 +109,10 @@ function readConfig(fileRef: ImportFileRef): CSpellSettings {
const r = cspellConfigExplorerSync.load(filename);
if (!r?.config) throw 'not found';
Object.assign(s, r.config);
validateRawConfig(s, fileRef);
} catch (err) {
fileRef.error = new ImportError(`Failed to read config file: "${filename}"`, err);
fileRef.error =
err instanceof ImportError ? err : new ImportError(`Failed to read config file: "${filename}"`, err);
}
s.__importRef = fileRef;
return s;
Expand Down Expand Up @@ -806,6 +813,40 @@ function normalizeSettingsGlobs(
};
}

function validationMessage(msg: string, fileRef: ImportFileRef) {
return msg + `\n File: "${fileRef.filename}"`;
}

function validateRawConfigVersion(config: CSpellUserSettings, fileRef: ImportFileRef) {
const { version } = config;
if (version === undefined || supportedCSpellConfigVersions.includes(version)) return;

if (!/^\d+(\.\d+)*$/.test(version)) {
logError(validationMessage(`Unsupported config file version: "${version}"`, fileRef));
return;
}

const msg =
version > currentSettingsFileVersion
? `Newer config file version found: "${version}". Supported version is "${currentSettingsFileVersion}"`
: `Legacy config file version found: "${version}". Upgrade to "${currentSettingsFileVersion}"`;

logWarning(validationMessage(msg, fileRef));
}

function validateRawConfigExports(config: CSpellUserSettings, fileRef: ImportFileRef) {
if ((<{ default: unknown }>config).default) {
throw new ImportError(
validationMessage('Module `export default` is not supported.\n Use `module.exports =` instead.', fileRef)
);
}
}

function validateRawConfig(config: CSpellUserSettings, fileRef: ImportFileRef): void {
const validations = [validateRawConfigExports, validateRawConfigVersion];
validations.forEach((fn) => fn(config, fileRef));
}

export const __testing__ = {
normalizeSettings,
};
8 changes: 5 additions & 3 deletions packages/cspell-lib/src/Settings/GlobalSettings.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Configstore from 'configstore';
import { mocked } from 'ts-jest/utils';
import { getLogger } from '../util/logger';
// eslint-disable-next-line jest/no-mocks-import
import {
clearData as clearConfigstore,
Expand All @@ -10,8 +11,9 @@ import {
} from '../__mocks__/configstore';
import { getGlobalConfigPath, getRawGlobalSettings, writeRawGlobalSettings } from './GlobalSettings';

const mockLog = jest.spyOn(console, 'log').mockImplementation();
const mockError = jest.spyOn(console, 'error').mockImplementation();
const logger = getLogger();
const mockLog = jest.spyOn(logger, 'log').mockImplementation();
const mockError = jest.spyOn(logger, 'error').mockImplementation();
const mockConfigstore = mocked(Configstore, true);

describe('Validate GlobalSettings', () => {
Expand Down Expand Up @@ -60,7 +62,7 @@ describe('Validate GlobalSettings', () => {
filename: undefined,
},
});
expect(mockLog).toHaveBeenCalledWith(expect.any(Error));
expect(mockError).toHaveBeenCalledWith(expect.any(Error));
});

test('writeRawGlobalSettings', () => {
Expand Down
73 changes: 37 additions & 36 deletions packages/cspell-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,56 @@
import * as ExclusionHelper from './exclusionHelper';
import { clearCachedSettingsFiles } from './Settings';
import * as Link from './Settings/link';
import { refreshDictionaryCache } from './SpellingDictionary';
import * as Text from './util/text';

export * from '@cspell/cspell-types';
export * from 'cspell-io';
export { ExcludeFilesGlobMap, ExclusionFunction } from './exclusionHelper';
export { getLanguagesForExt } from './LanguageIds';
export * from './Settings';
export * from '@cspell/cspell-types';
export { TextOffset, TextDocumentOffset } from './util/text';
export {
checkText,
CheckTextInfo,
IncludeExcludeFlag,
IncludeExcludeOptions,
TextInfoItem,
validateText,
} from './validator';
export { defaultFileName as defaultSettingsFilename } from './Settings';
export {
CompoundWordsMethod,
createSpellingDictionary,
getDictionary,
isSpellingDictionaryLoadError,
refreshDictionaryCache,
SpellingDictionary,
SuggestionCollector,
SuggestionResult,
SpellingDictionaryLoadError,
} from './SpellingDictionary';
export { combineTextAndLanguageSettings } from './Settings/TextDocumentSettings';
export { combineTextAndLanguageSettings as constructSettingsForText } from './Settings/TextDocumentSettings';
combineTextAndLanguageSettings,
combineTextAndLanguageSettings as constructSettingsForText,
} from './Settings/TextDocumentSettings';
export {
determineFinalDocumentSettings,
DetermineFinalDocumentSettingsResult,
Document,
fileToDocument,
isBinaryFile,
spellCheckDocument,
spellCheckFile,
SpellCheckFileOptions,
SpellCheckFileResult,
isBinaryFile,
} from './spellCheckFile';

import * as Text from './util/text';
import * as Link from './Settings/link';
export { Text, Link };
export {
CompoundWordsMethod,
createSpellingDictionary,
getDictionary,
isSpellingDictionaryLoadError,
refreshDictionaryCache,
SpellingDictionary,
SpellingDictionaryLoadError,
SuggestionCollector,
SuggestionResult,
} from './SpellingDictionary';
export * from './trace';
export { getLogger, Logger, setLogger } from './util/logger';
export { resolveFile } from './util/resolveFile';

import * as ExclusionHelper from './exclusionHelper';
export { TextDocumentOffset, TextOffset } from './util/text';
export {
checkText,
CheckTextInfo,
IncludeExcludeFlag,
IncludeExcludeOptions,
TextInfoItem,
validateText,
ValidationIssue,
} from './validator';
export { Text, Link };
export { ExclusionHelper };
export { ExcludeFilesGlobMap, ExclusionFunction } from './exclusionHelper';

export { getLanguagesForExt } from './LanguageIds';
export * from './trace';

import { clearCachedSettingsFiles } from './Settings';
import { refreshDictionaryCache } from './SpellingDictionary';

export async function clearCachedFiles(): Promise<void> {
await Promise.all([clearCachedSettingsFiles(), refreshDictionaryCache(0)]);
Expand Down
46 changes: 46 additions & 0 deletions packages/cspell-lib/src/util/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { mocked } from 'ts-jest/utils';
import { getLogger, log, logError, Logger, logWarning, setLogger } from './logger';

const logger: Logger = {
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};

const mockLogger = mocked(logger);

describe('logger', () => {
beforeEach(() => {
mockLogger.log.mockClear();
mockLogger.warn.mockClear();
mockLogger.error.mockClear();
});

test('logError', () => {
setLogger(logger);
const msg = 'Error Message';
logError(msg);
expect(mockLogger.error).toHaveBeenCalledWith(msg);
});

test('logWarning', () => {
setLogger(logger);
const msg = 'Warning Message';
logWarning(msg);
expect(mockLogger.warn).toHaveBeenCalledWith(msg);
});

test('log', () => {
setLogger(logger);
const msg = 'Log Message';
log(msg);
expect(mockLogger.log).toHaveBeenCalledWith(msg);
});

test('setLogger', () => {
setLogger(console);
expect(getLogger()).toBe(console);
expect(setLogger(logger)).toBe(console);
expect(getLogger()).toBe(logger);
});
});
50 changes: 48 additions & 2 deletions packages/cspell-lib/src/util/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,50 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/* eslint-disable @typescript-eslint/no-explicit-any */

type Console = typeof console;
export interface Logger {
log: Console['log'];
warn: Console['warn'];
error: Console['error'];
}

let _logger: Logger = console;

/**
* See `Console.error`
*/
export function logError(...args: any[]): void {
console.log(...args);
_logger.error(...args);
}

/**
* See `Console.warn`
*/
export function logWarning(...args: any[]): void {
_logger.warn(...args);
}

/**
* See `Console.log`
*/
export function log(...args: any[]): void {
_logger.log(...args);
}

/**
* Set the global cspell-lib logger
* @param logger - a logger like `console`
* @returns the old logger.
*/
export function setLogger(logger: Logger): Logger {
const oldLogger = _logger;
_logger = logger;
return oldLogger;
}

/**
* Get the current cspell-lib logger.
* @returns the current logger.
*/
export function getLogger(): Logger {
return _logger;
}
1 change: 1 addition & 0 deletions packages/cspell/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function getEmitters(options: Options): Emitters {
const emitters: InfoEmitter = {
Debug: !silent && debug ? (s) => console.info(chalk.cyan(s)) : nullEmitter,
Info: !silent && verbose ? (s) => console.info(chalk.yellow(s)) : nullEmitter,
Warning: (s) => console.info(chalk.yellow(s)),
};

function infoEmitter(message: string, msgType: MessageType): void {
Expand Down
9 changes: 6 additions & 3 deletions packages/cspell/src/emitters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface Issue extends TextDocumentOffset {
suggestions?: string[];
}

export type MessageType = 'Debug' | 'Info';
export type MessageType = 'Debug' | 'Info' | 'Warning';

export type MessageTypeLookup = {
[key in MessageType]: key;
Expand All @@ -16,6 +16,7 @@ export type MessageTypeLookup = {
export const MessageTypes: MessageTypeLookup = {
Debug: 'Debug',
Info: 'Info',
Warning: 'Warning',
};

export interface MessageEmitter {
Expand All @@ -26,12 +27,14 @@ export interface DebugEmitter {
(message: string): void;
}

type ErrorLike = Error | { message: string; name: string; toString: () => string };

export interface ErrorEmitterVoid {
(message: string, error: Error): void;
(message: string, error: ErrorLike): void;
}

export interface ErrorEmitterPromise {
(message: string, error: Error): Promise<void>;
(message: string, error: ErrorLike): Promise<void>;
}

type ErrorEmitter = ErrorEmitterVoid | ErrorEmitterPromise;
Expand Down
Loading

0 comments on commit e05aeff

Please sign in to comment.