Skip to content

Commit

Permalink
dev: Support eslint-plugin options (#2566)
Browse files Browse the repository at this point in the history
Options:
- ignoreImports
- checkIdentifiers
- checkStrings
- checkStringTemplates
- checkComments
  • Loading branch information
Jason3S committed Mar 11, 2022
1 parent 782f2d7 commit 4f65540
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 33 deletions.
Expand Up @@ -21,3 +21,10 @@ export async function listFiles() {
const entries = dirs.map(mapDir);
console.log(entries.join('\n'));
}

/**
* This function will coool the beans.
*/
export function cooolBeans() {
return 'ice cubes';
}
5 changes: 5 additions & 0 deletions packages/cspell-eslint-plugin/fixtures/with-errors/strings.ts
@@ -0,0 +1,5 @@
export const name = 'naaame';
export const stringDouble = "doen't";
export const template = `
This is a template with isssues. It uses ${name} and doesn't playy nice.
`;
Expand Up @@ -3,6 +3,26 @@
"additionalProperties": false,
"definitions": {},
"properties": {
"checkComments": {
"default": true,
"description": "Spell check comments",
"type": "boolean"
},
"checkIdentifiers": {
"default": true,
"description": "Spell check identifiers (variables names, function names, and class names)",
"type": "boolean"
},
"checkStringTemplates": {
"default": true,
"description": "Spell check template strings",
"type": "boolean"
},
"checkStrings": {
"default": true,
"description": "Spell check strings",
"type": "boolean"
},
"debugMode": {
"default": false,
"description": "Output debug logs",
Expand All @@ -13,6 +33,11 @@
"description": "Generate suggestions",
"type": "boolean"
},
"ignoreImports": {
"default": true,
"description": "Ignore import and require names",
"type": "boolean"
},
"numSuggestions": {
"default": 8,
"description": "Number of spelling suggestions to make.",
Expand All @@ -21,8 +46,7 @@
},
"required": [
"numSuggestions",
"generateSuggestions",
"debugMode"
"generateSuggestions"
],
"type": "object"
}
78 changes: 63 additions & 15 deletions packages/cspell-eslint-plugin/src/cspell-eslint-plugin.ts
Expand Up @@ -6,7 +6,7 @@ import type { Rule } from 'eslint';
// eslint-disable-next-line node/no-missing-import
import type { Comment, Identifier, Literal, Node, TemplateElement } from 'estree';
import { format } from 'util';
import { defaultOptions, type Options } from './options';
import { normalizeOptions } from './options';
import optionsSchema from './_auto_generated_/options.schema.json';

const schema = optionsSchema as unknown as Rule.RuleMetaData['schema'];
Expand Down Expand Up @@ -35,7 +35,7 @@ const meta: Rule.RuleMetaData = {
schema: [schema],
};

type ASTNode = Node | Comment;
type ASTNode = (Node | Comment) & Partial<Rule.NodeParentExtension>;

const defaultSettings: CSpellSettings = {
patterns: [
Expand All @@ -55,32 +55,43 @@ function log(...args: Parameters<typeof console.log>) {
}

function create(context: Rule.RuleContext): Rule.RuleListener {
const options: Options = context.options[0] || defaultOptions;
isDebugMode = options.debugMode;
const options = normalizeOptions(context.options[0]);
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);
validator.prepareSync();

function checkLiteral(node: Literal & Rule.NodeParentExtension) {
debugNode(node, node.value);
if (!options.checkStrings) return;
if (typeof node.value === 'string') {
if (options.ignoreImports && isImportOrRequired(node)) return;
debugNode(node, node.value);
checkNodeText(node, node.value);
}
}

function checkTemplateElement(node: TemplateElement & Rule.NodeParentExtension) {
if (!options.checkStringTemplates) return;
debugNode(node, node.value);
// console.log('Template: %o', node.value);
checkNodeText(node, node.value.cooked || node.value.raw);
}

function checkIdentifier(node: Identifier & Rule.NodeParentExtension) {
if (options.ignoreImports && isImportIdentifier(node)) {
importedIdentifiers.add(node.name);
return;
}
if (!options.checkIdentifiers) return;
if (importedIdentifiers.has(node.name)) return;
debugNode(node, node.name);
checkNodeText(node, node.name);
}

function checkComment(node: Comment) {
if (!options.checkComments) return;
debugNode(node, node.value);
checkNodeText(node, node.value);
}
Expand All @@ -91,26 +102,39 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
const adj = node.type === 'Literal' ? 1 : 0;
const range = [node.range[0] + adj, node.range[1] - adj] as const;

const scope = inheritance(node);
const scope: string[] = calcScope(node);
const result = validator.checkText(range, text, scope);
result.forEach((issue) => reportIssue(issue));
}

function calcScope(_node: ASTNode): string[] {
// inheritance(node);
return [];
}

function isImportIdentifier(node: ASTNode): boolean {
const parent = node.parent;
if (node.type !== 'Identifier' || !parent) return false;
return (
(parent.type === 'ImportSpecifier' && parent.imported === node) ||
(parent.type === 'ExportSpecifier' && parent.local === node)
);
}

function reportIssue(issue: ValidationIssue) {
// const messageId = issue.isFlagged ? 'cspell-forbidden-word' : 'cspell-unknown-word';
const messageId: MessageIds = issue.isFlagged ? 'wordForbidden' : 'wordUnknown';
const data = {
word: issue.text,
};
const code = context.getSourceCode();
const a = issue.offset;
const b = issue.offset + (issue.length || issue.text.length);
const start = code.getLocFromIndex(a);
const end = code.getLocFromIndex(b);
const loc = { start, end };
const start = issue.offset;
const end = issue.offset + (issue.length || issue.text.length);
const startPos = code.getLocFromIndex(start);
const endPos = code.getLocFromIndex(end);
const loc = { start: startPos, end: endPos };

function fixFactory(word: string): Rule.ReportFixer {
return (fixer) => fixer.replaceTextRange([a, b], word);
return (fixer) => fixer.replaceTextRange([start, end], word);
}

function createSug(word: string): Rule.SuggestionReportDescriptor {
Expand Down Expand Up @@ -159,6 +183,14 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
const extra = node.source === child ? '.source' : '';
return node.type + extra;
}
if (node.type === 'ExportSpecifier') {
const extra = node.exported === child ? '.exported' : node.local === child ? '.local' : '';
return node.type + extra;
}
if (node.type === 'ExportNamedDeclaration') {
const extra = node.source === child ? '.source' : '';
return node.type + extra;
}
if (node.type === 'Property') {
const extra = node.key === child ? 'key' : node.value === child ? 'value' : '';
return [node.type, node.kind, extra].join('.');
Expand All @@ -179,6 +211,10 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
const extra = node.id === child ? 'id' : node.body === child ? 'body' : 'superClass';
return node.type + '.' + extra;
}
if (node.type === 'CallExpression') {
const extra = node.callee === child ? 'callee' : 'arguments';
return node.type + '.' + extra;
}
if (node.type === 'Literal') {
return tagLiteral(node);
}
Expand All @@ -200,6 +236,18 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
return inheritance(node).join(' ');
}

function isFunctionCall(node: ASTNode | undefined, name: string): boolean {
return node?.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name;
}

function isRequireCall(node: ASTNode | undefined) {
return isFunctionCall(node, 'require');
}

function isImportOrRequired(node: ASTNode) {
return isRequireCall(node.parent) || (node.parent?.type === 'ImportDeclaration' && node.parent.source === node);
}

function debugNode(node: ASTNode, value: unknown) {
if (!isDebugMode) return;
const val = format('%o', value);
Expand Down Expand Up @@ -229,8 +277,8 @@ export const rules: PluginRules = {
};

function logContext(context: Rule.RuleContext) {
log('Source code: \n ************************ \n\n');
log(context.getSourceCode().text);
log('\n\n************************');
// log(context.getSourceCode().text);
log(`
id: ${context.id}
Expand Down
52 changes: 43 additions & 9 deletions packages/cspell-eslint-plugin/src/index.test.ts
@@ -1,5 +1,5 @@
import { RuleTester } from 'eslint';
import * as rule from './index';
import * as Rule from './index';
import * as fs from 'fs';
import * as path from 'path';

Expand All @@ -11,6 +11,7 @@ const parsers: Record<string, string | undefined> = {
};

type CachedSample = RuleTester.ValidTestCase;
type Options = Partial<Rule.Options>;

const sampleFiles = new Map<string, CachedSample>();

Expand All @@ -35,16 +36,33 @@ const ruleTester = new RuleTester({
],
});

ruleTester.run('cspell', rule.rules.spellchecker, {
valid: [readSample('sample.js'), readSample('sample.ts'), readSample('sampleESM.mjs')],
ruleTester.run('cspell', Rule.rules.spellchecker, {
valid: [
readSample('sample.js'),
readSample('sample.ts'),
readSample('sampleESM.mjs'),
readFix('with-errors/strings.ts', { checkStrings: false, checkStringTemplates: false }),
],
invalid: [
// cspell:ignore Guuide Gallaxy BADD functionn
// cspell:ignore Guuide Gallaxy BADD functionn coool
readInvalid('with-errors/sampleESM.mjs', [
'Unknown word: "Guuide"',
'Unknown word: "Gallaxy"',
'Unknown word: "BADD"',
'Unknown word: "functionn"',
'Unknown word: "coool"',
'Unknown word: "coool"',
]),
readInvalid(
'with-errors/sampleESM.mjs',
['Unknown word: "Guuide"', 'Unknown word: "Gallaxy"', 'Unknown word: "functionn"', 'Unknown word: "coool"'],
{ checkIdentifiers: false }
),
readInvalid(
'with-errors/sampleESM.mjs',
['Unknown word: "Guuide"', 'Unknown word: "Gallaxy"', 'Unknown word: "BADD"', 'Unknown word: "coool"'],
{ checkComments: false }
),
// cspell:ignore Montj Todayy Yaar Aprill Februarry gooo weeek
readInvalid('with-errors/sampleTemplateString.mjs', [
{ message: 'Unknown word: "Todayy"' },
Expand All @@ -55,14 +73,27 @@ ruleTester.run('cspell', rule.rules.spellchecker, {
'Unknown word: "gooo"',
'Unknown word: "weeek"',
]),
// cspell:ignore naaame doen't isssues playy
readInvalid('with-errors/strings.ts', [
'Unknown word: "naaame"',
'Unknown word: "doen\'t"',
'Unknown word: "isssues"',
'Unknown word: "playy"',
]),
readInvalid('with-errors/strings.ts', ['Unknown word: "isssues"', 'Unknown word: "playy"'], {
checkStrings: false,
}),
readInvalid('with-errors/strings.ts', ['Unknown word: "naaame"', 'Unknown word: "doen\'t"'], {
checkStringTemplates: false,
}),
],
});

function resolveFromMonoRepo(file: string): string {
return path.resolve(root, file);
}

function readFix(_filename: string): CachedSample {
function readFix(_filename: string, options?: Options): CachedSample {
const s = sampleFiles.get(_filename);
if (s) return s;

Expand All @@ -73,6 +104,9 @@ function readFix(_filename: string): CachedSample {
code,
filename,
};
if (options) {
sample.options = [options];
}

const parser = parsers[path.extname(filename)];
if (parser) {
Expand All @@ -82,12 +116,12 @@ function readFix(_filename: string): CachedSample {
return sample;
}

function readSample(sampleFile: string) {
return readFix(path.join('samples', sampleFile));
function readSample(sampleFile: string, options?: Options) {
return readFix(path.join('samples', sampleFile), options);
}

function readInvalid(filename: string, errors: RuleTester.InvalidTestCase['errors']) {
const sample = readFix(filename);
function readInvalid(filename: string, errors: RuleTester.InvalidTestCase['errors'], options?: Options) {
const sample = readFix(filename, options);
return {
...sample,
errors,
Expand Down
3 changes: 2 additions & 1 deletion packages/cspell-eslint-plugin/src/index.ts
@@ -1 +1,2 @@
export * from './cspell-eslint-plugin';
export { rules, configs } from './cspell-eslint-plugin';
export type { Options } from './options';

0 comments on commit 4f65540

Please sign in to comment.