Skip to content

Commit

Permalink
fix: ESLint Plugin to support ignoring imported properties (#2573)
Browse files Browse the repository at this point in the history
* test: move the test fixture files.
* fix: do not spell check imported properties
* fix: support ignoring imported properties.
* fix: Make sure import aliases are checked.
* Update index.test.ts
  • Loading branch information
Jason3S committed Mar 13, 2022
1 parent 2e3e60d commit ee4cc20
Show file tree
Hide file tree
Showing 18 changed files with 228 additions and 29 deletions.
29 changes: 20 additions & 9 deletions packages/cspell-eslint-plugin/README.md
Expand Up @@ -21,30 +21,36 @@ This plugin is still in active development. Due to the nature of how files are p

## Options

```ts
````ts
interface Options {
/**
* Number of spelling suggestions to make.
* @default 8
*/
numSuggestions: number;

/**
* Generate suggestions
* @default true
*/
generateSuggestions: boolean;

/**
* Output debug logs
* @default false
*/
debugMode?: boolean;
/**
* Ignore import and require names
* @default true
*/
ignoreImports?: boolean;
/**
* Ignore the properties of imported variables, structures, and types.
*
* Example:
* ```
* import { example } from 'third-party';
*
* const msg = example.property; // `property` is not spell checked.
* ```
*
* @default true
*/
ignoreImportProperties?: boolean;
/**
* Spell check identifiers (variables names, function names, and class names)
* @default true
Expand All @@ -65,8 +71,13 @@ interface Options {
* @default true
*/
checkComments?: boolean;
/**
* Output debug logs
* @default false
*/
debugMode?: boolean;
}
```
````

Example:

Expand Down
21 changes: 21 additions & 0 deletions packages/cspell-eslint-plugin/fixtures/with-errors/creepyData.ts
@@ -0,0 +1,21 @@
export interface CreepyExpressions {
muawhahaha: string;
grrrrr: string;
uuuug: string;
}

export const expressions: CreepyExpressions = {
muawhahaha: 'muawhahaha',
grrrrr: 'grrrrr',
uuuug: 'uuuug',
};

export const muawhahaha = expressions.muawhahaha;
export const uuug = expressions.uuuug;
export const grrr = expressions.grrrrr;

export enum ExpressionCategory {
MUAWHAHAHA = 0,
GRRRRRR,
UUUUUG,
}
@@ -0,0 +1,5 @@
import { uuug as uuug, muawhahaha as evilLaugh, grrr } from './creepyData';

console.log(uuug);
console.log(evilLaugh);
console.log(grrr);
7 changes: 7 additions & 0 deletions packages/cspell-eslint-plugin/fixtures/with-errors/imports.ts
@@ -0,0 +1,7 @@
import { muawhahaha, expressions } from './creepyData';
import * as creepy from './creepyData';

console.log(creepy.expressions.grrrrr);
console.log(creepy.muawhahaha);
console.log(muawhahaha);
console.log(expressions.uuuug);
Expand Up @@ -10,7 +10,7 @@
},
"checkIdentifiers": {
"default": true,
"description": "Spell check identifiers (variables names, function names, and class names)",
"description": "Spell check identifiers (variables names, function names, class names, etc.)",
"type": "boolean"
},
"checkStringTemplates": {
Expand All @@ -33,6 +33,11 @@
"description": "Generate suggestions",
"type": "boolean"
},
"ignoreImportProperties": {
"default": true,
"description": "Ignore the properties of imported variables, structures, and types.\n\nExample: ``` import { example } from 'third-party';\n\nconst msg = example.property; // `property` is not spell checked. ```",
"type": "boolean"
},
"ignoreImports": {
"default": true,
"description": "Ignore import and require names",
Expand Down
80 changes: 73 additions & 7 deletions packages/cspell-eslint-plugin/src/cspell-eslint-plugin.ts
Expand Up @@ -4,7 +4,7 @@ import assert from 'assert';
import { createTextDocument, CSpellSettings, DocumentValidator, ValidationIssue } from 'cspell-lib';
import type { Rule } from 'eslint';
// eslint-disable-next-line node/no-missing-import
import type { Comment, Identifier, Literal, Node, TemplateElement } from 'estree';
import type { Comment, Identifier, Literal, Node, TemplateElement, ImportSpecifier } from 'estree';
import { format } from 'util';
import { normalizeOptions } from './options';
import optionsSchema from './_auto_generated_/options.schema.json';
Expand Down Expand Up @@ -56,6 +56,7 @@ function log(...args: Parameters<typeof console.log>) {

function create(context: Rule.RuleContext): Rule.RuleListener {
const options = normalizeOptions(context.options[0]);
const toIgnore = new Set<string>();
const importedIdentifiers = new Set<string>();
isDebugMode = options.debugMode || false;
isDebugMode && logContext(context);
Expand All @@ -66,8 +67,9 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
function checkLiteral(node: Literal & Rule.NodeParentExtension) {
if (!options.checkStrings) return;
if (typeof node.value === 'string') {
if (options.ignoreImports && isImportOrRequired(node)) return;
debugNode(node, node.value);
if (options.ignoreImports && isImportOrRequired(node)) return;
if (options.ignoreImportProperties && isImportedProperty(node)) return;
checkNodeText(node, node.value);
}
}
Expand All @@ -80,13 +82,25 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
}

function checkIdentifier(node: Identifier & Rule.NodeParentExtension) {
if (options.ignoreImports && isImportIdentifier(node)) {
importedIdentifiers.add(node.name);
return;
debugNode(node, node.name);
if (options.ignoreImports) {
if (isRawImportIdentifier(node)) {
toIgnore.add(node.name);
return;
}
if (isImportIdentifier(node)) {
importedIdentifiers.add(node.name);
if (isLocalImportIdentifierUnique(node)) {
checkNodeText(node, node.name);
}
return;
} else if (options.ignoreImportProperties && isImportedProperty(node)) {
return;
}
}
if (!options.checkIdentifiers) return;
if (importedIdentifiers.has(node.name)) return;
debugNode(node, node.name);
if (toIgnore.has(node.name) && !isObjectProperty(node)) return;
if (skipCheckForRawImportIdentifiers(node)) return;
checkNodeText(node, node.name);
}

Expand All @@ -113,6 +127,17 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
}

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

function isRawImportIdentifier(node: ASTNode): boolean {
const parent = node.parent;
if (node.type !== 'Identifier' || !parent) return false;
return (
Expand All @@ -121,6 +146,34 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
);
}

function isLocalImportIdentifierUnique(node: ASTNode): boolean {
const parent = getImportParent(node);
if (!parent) return true;
const { imported, local } = parent;
if (imported.name !== local.name) return true;
return imported.range?.[0] !== local.range?.[0] && imported.range?.[1] !== local.range?.[1];
}

function getImportParent(node: ASTNode): ImportSpecifier | undefined {
const parent = node.parent;
return parent?.type === 'ImportSpecifier' ? parent : undefined;
}

function skipCheckForRawImportIdentifiers(node: ASTNode): boolean {
if (options.ignoreImports) return false;
const parent = getImportParent(node);
return !!parent && parent.imported === node && !isLocalImportIdentifierUnique(node);
}

function isImportedProperty(node: ASTNode): boolean {
const obj = findOriginObject(node);
return !!obj && obj.type === 'Identifier' && importedIdentifiers.has(obj.name);
}

function isObjectProperty(node: ASTNode): boolean {
return node.parent?.type === 'MemberExpression';
}

function reportIssue(issue: ValidationIssue) {
const messageId: MessageIds = issue.isFlagged ? 'wordForbidden' : 'wordUnknown';
const data = {
Expand Down Expand Up @@ -236,6 +289,19 @@ function create(context: Rule.RuleContext): Rule.RuleListener {
return inheritance(node).join(' ');
}

/**
* find the origin of a member expression
*/
function findOriginObject(node: ASTNode): ASTNode | undefined {
const parent = node.parent;
if (parent?.type !== 'MemberExpression' || parent.property !== node) return undefined;
let obj = parent.object;
while (obj.type === 'MemberExpression') {
obj = obj.object;
}
return obj;
}

function isFunctionCall(node: ASTNode | undefined, name: string): boolean {
return node?.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name;
}
Expand Down
33 changes: 33 additions & 0 deletions packages/cspell-eslint-plugin/src/index.test.ts
Expand Up @@ -42,6 +42,7 @@ ruleTester.run('cspell', Rule.rules.spellchecker, {
readSample('sample.ts'),
readSample('sampleESM.mjs'),
readFix('with-errors/strings.ts', { checkStrings: false, checkStringTemplates: false }),
readFix('with-errors/imports.ts'),
],
invalid: [
// cspell:ignore Guuide Gallaxy BADD functionn coool
Expand Down Expand Up @@ -86,6 +87,38 @@ ruleTester.run('cspell', Rule.rules.spellchecker, {
readInvalid('with-errors/strings.ts', ['Unknown word: "naaame"', 'Unknown word: "doen\'t"'], {
checkStringTemplates: false,
}),
// cspell:ignore muawhahaha grrrrr uuuug
readInvalid(
'with-errors/imports.ts',
[
'Unknown word: "muawhahaha"',
'Unknown word: "grrrrr"',
'Unknown word: "muawhahaha"',
'Unknown word: "muawhahaha"',
'Unknown word: "uuuug"',
],
{ ignoreImports: false }
),
readInvalid(
'with-errors/imports.ts',
['Unknown word: "grrrrr"', 'Unknown word: "muawhahaha"', 'Unknown word: "uuuug"'],
{ ignoreImportProperties: false }
),
// cspell:ignore uuug grrr
readInvalid('with-errors/importAlias.ts', ['Unknown word: "uuug"']),
readInvalid('with-errors/importAlias.ts', ['Unknown word: "uuug"'], { ignoreImportProperties: false }),
readInvalid(
'with-errors/importAlias.ts',
[
'Unknown word: "uuug"',
'Unknown word: "uuug"',
'Unknown word: "muawhahaha"',
'Unknown word: "grrr"',
'Unknown word: "uuug"',
'Unknown word: "grrr"',
],
{ ignoreImports: false }
),
],
});

Expand Down
24 changes: 19 additions & 5 deletions packages/cspell-eslint-plugin/src/options.ts
Expand Up @@ -25,7 +25,20 @@ export interface Check {
*/
ignoreImports?: boolean;
/**
* Spell check identifiers (variables names, function names, and class names)
* Ignore the properties of imported variables, structures, and types.
*
* Example:
* ```
* import { example } from 'third-party';
*
* const msg = example.property; // `property` is not spell checked.
* ```
*
* @default true
*/
ignoreImportProperties?: boolean;
/**
* Spell check identifiers (variables names, function names, class names, etc.)
* @default true
*/
checkIdentifiers?: boolean;
Expand All @@ -46,22 +59,23 @@ export interface Check {
checkComments?: boolean;
}

export const defaultCheckOptions: Check = {
export const defaultCheckOptions: Required<Check> = {
checkComments: true,
checkIdentifiers: true,
checkStrings: true,
checkStringTemplates: true,
ignoreImports: true,
ignoreImportProperties: true,
};

export const defaultOptions: Options = {
export const defaultOptions: Required<Options> = {
...defaultCheckOptions,
numSuggestions: 8,
generateSuggestions: true,
debugMode: false,
};

export function normalizeOptions(opts: Options | undefined): Options {
const options: Options = Object.assign({}, defaultOptions, opts || {});
export function normalizeOptions(opts: Options | undefined): Required<Options> {
const options: Required<Options> = Object.assign({}, defaultOptions, opts || {});
return options;
}
8 changes: 8 additions & 0 deletions test-packages/test-cspell-eslint-plugin/.eslintrc.debug.js
@@ -0,0 +1,8 @@
/**
* @type { import("eslint").Linter.Config }
*/
const config = {
extends: ['./.eslintrc.js', 'plugin:@cspell/debug'],
};

module.exports = config;
3 changes: 1 addition & 2 deletions test-packages/test-cspell-eslint-plugin/.eslintrc.js
Expand Up @@ -13,8 +13,7 @@ const config = {
'plugin:import/warnings',
'plugin:promise/recommended',
'plugin:prettier/recommended',
// 'plugin:@cspell/recommended',
'plugin:@cspell/debug',
'plugin:@cspell/recommended',
],
ignorePatterns: ['**/*.d.ts', '**/*.map', '**/coverage/**', '**/dist/**', '**/node_modules/**'],
parserOptions: {
Expand Down
21 changes: 21 additions & 0 deletions test-packages/test-cspell-eslint-plugin/fixtures/creepyData.ts
@@ -0,0 +1,21 @@
export interface CreepyExpressions {
muawhahaha: string;
grrrrr: string;
uuuug: string;
}

export const expressions: CreepyExpressions = {
muawhahaha: 'muawhahaha',
grrrrr: 'grrrrr',
uuuug: 'uuuug',
};

export const muawhahaha = expressions.muawhahaha;
export const uuug = expressions.uuuug;
export const grrr = expressions.grrrrr;

export enum ExpressionCategory {
MUAWHAHAHA = 0,
GRRRRRR,
UUUUUG,
}
@@ -0,0 +1,5 @@
import { uuug as uuug, muawhahaha as evilLaugh, grrr } from './creepyData';

console.log(uuug);
console.log(evilLaugh);
console.log(grrr);

0 comments on commit ee4cc20

Please sign in to comment.