Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ESLint Plugin to support ignoring imported properties #2573

Merged
merged 5 commits into from Mar 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);