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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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.
`;
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './cspell-eslint-plugin';
export { rules, configs } from './cspell-eslint-plugin';
export type { Options } from './options';
Loading

0 comments on commit 4f65540

Please sign in to comment.