Skip to content

Commit

Permalink
Merge branch 'main' into lens-flaky-test
Browse files Browse the repository at this point in the history
  • Loading branch information
kibanamachine authored May 11, 2022
2 parents 882e75d + 4e2989b commit bee46f3
Show file tree
Hide file tree
Showing 96 changed files with 556 additions and 163 deletions.
1 change: 1 addition & 0 deletions packages/elastic-eslint-config-kibana/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,6 @@ module.exports = {
'@kbn/eslint/no_this_in_property_initializers': 'error',
'@kbn/imports/no_unresolvable_imports': 'error',
'@kbn/imports/uniform_imports': 'error',
'@kbn/imports/no_unused_imports': 'error',
},
};
6 changes: 5 additions & 1 deletion packages/kbn-eslint-plugin-imports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ Config example:

This config will find any import of `@kbn/kitchen-sink` which specifically references the `Spatula` or `isSpatula` exports, remove the old exports from the import (potentially removing the entire import), and add a new import after the previous following it's style pointing to the new package.

The auto-fixer here covers the vast majority of import styles in the repository but might not cover everything, including `import * as Namespace from '@kbn/kitchen-sink'`. Imports like this will need to be found and updated manually, though TypeScript should be able to find the vast majority of those.
The auto-fixer here covers the vast majority of import styles in the repository but might not cover everything, including `import * as Namespace from '@kbn/kitchen-sink'`. Imports like this will need to be found and updated manually, though TypeScript should be able to find the vast majority of those.

## `@kbn/imports/no_unused_imports`

This rule finds imports that are unused and provides an auto-fix to remove them. When ESLint appears to be running in an editor, as defined by [`helpers/running_in_editor.ts`](src/helpers/running_in_editor.ts), this rule provided suggestions instead of fixes so that the removals are not applied automatically in case you are debugging, returning early, or something else which makes ESLint think that the import is unused when it isn't. On CI and in the pre-commit hook though, this fix will be applied automatically.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

export const RUNNING_IN_EDITOR =
!process.env.IS_KIBANA_PRECOMIT_HOOK &&
// vscode sets this in the env for all workers
!!process.env.VSCODE_CWD ||
// MacOS sets this for intellij processes, not sure if it works in webstorm but we could expand this check later
!!process.env.__CFBundleIdentifier?.startsWith('com.jetbrains.intellij');
(!!process.env.VSCODE_CWD ||
// MacOS sets this for intellij processes, not sure if it works in webstorm but we could expand this check later
!!process.env.__CFBundleIdentifier?.startsWith('com.jetbrains.intellij'));
2 changes: 2 additions & 0 deletions packages/kbn-eslint-plugin-imports/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './get_import_resolver';
import { NoUnresolvableImportsRule } from './rules/no_unresolvable_imports';
import { UniformImportsRule } from './rules/uniform_imports';
import { ExportsMovedPackagesRule } from './rules/exports_moved_packages';
import { NoUnusedImportsRule } from './rules/no_unused_imports';

/**
* Custom ESLint rules, add `'@kbn/eslint-plugin-imports'` to your eslint config to use them
Expand All @@ -19,4 +20,5 @@ export const rules = {
no_unresolvable_imports: NoUnresolvableImportsRule,
uniform_imports: UniformImportsRule,
exports_moved_packages: ExportsMovedPackagesRule,
no_unused_imports: NoUnusedImportsRule,
};
151 changes: 151 additions & 0 deletions packages/kbn-eslint-plugin-imports/src/rules/no_unused_imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { RuleTester } from 'eslint';
import { NoUnusedImportsRule } from './no_unused_imports';
import dedent from 'dedent';

const fmt = (str: TemplateStringsArray) => dedent(str) + '\n';

const tsTester = [
'@typescript-eslint/parser',
new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
ecmaFeatures: {
jsx: true,
},
},
}),
] as const;

const babelTester = [
'@babel/eslint-parser',
new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
requireConfigFile: false,
babelOptions: {
presets: ['@kbn/babel-preset/node_preset'],
},
},
}),
] as const;

for (const [name, tester] of [tsTester, babelTester]) {
describe(name, () => {
tester.run('@kbn/imports/no_unused_imports', NoUnusedImportsRule, {
valid: [
{
filename: 'foo.ts',
code: fmt`
import { foo, bar as Bar } from 'new'
use(foo, Bar)
`,
},
{
filename: 'foo.ts',
code: fmt`
import Old from 'old'
use(Old)
`,
},
],

invalid: [
{
filename: 'foo.ts',
code: fmt`
import { foo, bar as Bar } from 'old'
`,
errors: [
{
line: 1,
message: 'All imports from "old" are unused and should be removed',
},
],
output: '',
},
{
filename: 'foo.ts',
code: fmt`
import type { foo, bar as Bar } from 'old'
`,
errors: [
{
line: 1,
message: 'All imports from "old" are unused and should be removed',
},
],
output: '',
},
{
filename: 'foo.ts',
code: fmt`
import type { foo, bar as Bar } from 'old'
use(foo)
`,
errors: [
{
line: 1,
message: 'Bar is unused and should be removed',
},
],
output: fmt`
import type { foo, } from 'old'
use(foo)
`,
},
{
filename: 'foo.ts',
code: fmt`
import type { foo, bar as Bar } from 'old'
use(Bar)
`,
errors: [
{
line: 1,
message: 'foo is unused and should be removed',
},
],
output: fmt`
import type { bar as Bar } from 'old'
use(Bar)
`,
},
{
filename: 'foo.ts',
code: fmt`
// @ts-expect-error
// @ts-ignore
// foo message
// eslint-disable-next-line some-other-rule
import type { foo, bar as Bar } from 'old'
`,
errors: [
{
line: 4,
message: `Definition for rule 'some-other-rule' was not found.`,
},
{
line: 5,
message: 'All imports from "old" are unused and should be removed',
},
],
output: fmt`
// foo message
`,
},
],
});
});
}
184 changes: 184 additions & 0 deletions packages/kbn-eslint-plugin-imports/src/rules/no_unused_imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Rule, Scope, AST } from 'eslint';
import type { Comment } from 'estree';
import * as T from '@babel/types';
import { TSESTree } from '@typescript-eslint/typescript-estree';

import { RUNNING_IN_EDITOR } from '../helpers/running_in_editor';

type WithParent<T> = T & { parent?: WithParent<T> };
type SomeNode = WithParent<T.Node> | TSESTree.Node;
type SomeImportNode = NonNullable<ReturnType<typeof findImportParent>>;

function findImportParent(def: Scope.Definition) {
let cursor: SomeNode | undefined = def.node;
while (cursor) {
if (
T.isImportDeclaration(cursor) ||
cursor.type === TSESTree.AST_NODE_TYPES.ImportDeclaration
) {
return cursor;
}
cursor = cursor.parent;
}
return;
}

function isEslintUsed(variable: any) {
return !!variable.eslintUsed;
}

function findUnusedImportDefs(globalScope: Scope.Scope) {
if (globalScope.type !== 'global') {
throw new Error('pass the global scope');
}

const unused = [];

for (const scope of globalScope.childScopes) {
if (scope.type !== 'module') {
continue;
}

for (const variable of scope.variables) {
if (variable.references.length > 0 || isEslintUsed(variable)) {
continue;
}

for (const def of variable.defs) {
const importParent = findImportParent(def);
if (importParent) {
unused.push({
def,
importParent,
});
}
}
}
}

return unused;
}

function isTsOrEslintIgnore(comment: Comment) {
const value = comment.value.trim();
return (
value.startsWith('@ts-ignore') ||
value.startsWith('@ts-expect-error') ||
value.startsWith('eslint-disable')
);
}

export const NoUnusedImportsRule: Rule.RuleModule = {
meta: {
fixable: 'code',
docs: {
url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.md#kbnimportsno_unused_imports',
},
},
create(context) {
const source = context.getSourceCode();

function getRange(
nodeA: { loc?: AST.SourceLocation | null },
nodeB: { loc?: AST.SourceLocation | null } | number = nodeA
): AST.Range {
if (!nodeA.loc) {
throw new Error('unable to use babel AST nodes without locations');
}
const nodeBLoc = typeof nodeB === 'number' ? nodeB : nodeB.loc;
if (nodeBLoc == null) {
throw new Error('unable to use babel AST nodes without locations');
}
return [
source.getIndexFromLoc(nodeA.loc.start),
typeof nodeBLoc === 'number'
? source.getIndexFromLoc(nodeA.loc.end) + nodeBLoc
: source.getIndexFromLoc(nodeBLoc.end),
];
}

function report(
node: SomeNode,
msg: string,
fix: (fixer: Rule.RuleFixer) => IterableIterator<Rule.Fix>
) {
context.report({
node: node as any,
message: msg,
...(RUNNING_IN_EDITOR
? {
suggest: [
{
desc: 'Remove',
fix,
},
],
}
: {
fix,
}),
});
}

return {
'Program:exit': () => {
const unusedByImport = new Map<SomeImportNode, Scope.Definition[]>();
for (const { importParent, def } of findUnusedImportDefs(context.getScope())) {
const group = unusedByImport.get(importParent);
if (group) {
group.push(def);
} else {
unusedByImport.set(importParent, [def]);
}
}

for (const [importParent, defs] of unusedByImport) {
if (importParent.specifiers.length === defs.length) {
report(
importParent,
`All imports from "${importParent.source.value}" are unused and should be removed`,
function* (fixer) {
// remove entire import including trailing newline if it's detected
const textPlus1 = source.getText(importParent as any, 0, 1);
const range = getRange(importParent, textPlus1.endsWith('\n') ? 1 : importParent);

// if the import is preceeded by one or more eslint/tslint disable comments then remove them
for (const comment of source.getCommentsBefore(importParent as any)) {
if (isTsOrEslintIgnore(comment)) {
const cRange = getRange(comment);
yield fixer.removeRange(
source.text[cRange[1]] !== '\n' ? cRange : getRange(comment, 1)
);
}
}

yield fixer.removeRange(range);
}
);
} else {
for (const def of defs) {
report(
def.node,
`${def.name.name} is unused and should be removed`,
function* (fixer) {
const nextToken = source.getTokenAfter(def.node);
yield fixer.removeRange(
getRange(def.node, nextToken?.value === ',' ? nextToken : undefined)
);
}
);
}
}
}
},
};
},
};
2 changes: 0 additions & 2 deletions packages/kbn-i18n-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
* Side Public License, v 1.
*/

// eslint-disable-next-line @kbn/eslint/module_migration
import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl';
// eslint-disable-next-line @kbn/eslint/module_migration
export type { InjectedIntl, InjectedIntlProps } from 'react-intl';

Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-test/types/ftr_globals/mocha.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { Suite } from 'mocha';
import 'mocha';

declare module 'mocha' {
interface Suite {
Expand Down
Loading

0 comments on commit bee46f3

Please sign in to comment.