Skip to content

Commit

Permalink
fix(eslint-plugin): [keyword-spacing] unexpected space before/after i…
Browse files Browse the repository at this point in the history
…n `import type` (#6095)
  • Loading branch information
Omri Luzon committed Dec 2, 2022
1 parent 5d49d5d commit 98caa92
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 18 deletions.
55 changes: 43 additions & 12 deletions packages/eslint-plugin/src/rules/keyword-spacing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_TOKEN_TYPES } from '@typescript-eslint/utils';
import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';

import * as util from '../util';
import { getESLintCoreRule } from '../util/getESLintCoreRule';
Expand All @@ -9,6 +9,25 @@ const baseRule = getESLintCoreRule('keyword-spacing');
export type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const baseSchema = Array.isArray(baseRule.meta.schema)
? baseRule.meta.schema[0]
: baseRule.meta.schema;
const schema = util.deepMerge(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- https://github.com/microsoft/TypeScript/issues/17002
baseSchema,
{
properties: {
overrides: {
properties: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
type: baseSchema.properties.overrides.properties.import,
},
},
},
},
);

export default util.createRule<Options, MessageIds>({
name: 'keyword-spacing',
meta: {
Expand All @@ -20,12 +39,12 @@ export default util.createRule<Options, MessageIds>({
},
fixable: 'whitespace',
hasSuggestions: baseRule.meta.hasSuggestions,
schema: baseRule.meta.schema,
schema: [schema],
messages: baseRule.meta.messages,
},
defaultOptions: [{}],

create(context, [{ after }]) {
create(context, [{ after, overrides }]) {
const sourceCode = context.getSourceCode();
const baseRules = baseRule.create(context);
return {
Expand Down Expand Up @@ -54,25 +73,37 @@ export default util.createRule<Options, MessageIds>({
'ImportDeclaration[importKind=type]'(
node: TSESTree.ImportDeclaration,
): void {
const { type: typeOptionOverride = {} } = overrides ?? {};
const typeToken = sourceCode.getFirstToken(node, { skip: 1 })!;
const punctuatorToken = sourceCode.getTokenAfter(typeToken)!;
if (
node.specifiers?.[0]?.type === AST_NODE_TYPES.ImportDefaultSpecifier
) {
return;
}
const spacesBetweenTypeAndPunctuator =
punctuatorToken.range[0] - typeToken.range[1];
if (after && spacesBetweenTypeAndPunctuator === 0) {
if (
(typeOptionOverride.after ?? after) === true &&
spacesBetweenTypeAndPunctuator === 0
) {
context.report({
loc: punctuatorToken.loc,
messageId: 'expectedBefore',
data: { value: punctuatorToken.value },
loc: typeToken.loc,
messageId: 'expectedAfter',
data: { value: 'type' },
fix(fixer) {
return fixer.insertTextBefore(punctuatorToken, ' ');
return fixer.insertTextAfter(typeToken, ' ');
},
});
}
if (!after && spacesBetweenTypeAndPunctuator > 0) {
if (
(typeOptionOverride.after ?? after) === false &&
spacesBetweenTypeAndPunctuator > 0
) {
context.report({
loc: punctuatorToken.loc,
messageId: 'unexpectedBefore',
data: { value: punctuatorToken.value },
loc: typeToken.loc,
messageId: 'unexpectedAfter',
data: { value: 'type' },
fix(fixer) {
return fixer.removeRange([
typeToken.range[1],
Expand Down
152 changes: 146 additions & 6 deletions packages/eslint-plugin/tests/rules/keyword-spacing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,122 @@ ruleTester.run('keyword-spacing', rule, {
},
{
code: 'import type { foo } from "foo";',
options: [{ overrides: { as: {} } }],
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: "import type * as Foo from 'foo'",
options: [{ overrides: { as: {} } }],
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'import type { SavedQueries } from "./SavedQueries.js";',
options: [
{
before: true,
after: false,
overrides: {
else: { after: true },
return: { after: true },
try: { after: true },
catch: { after: false },
case: { after: true },
const: { after: true },
throw: { after: true },
let: { after: true },
do: { after: true },
of: { after: true },
as: { after: true },
finally: { after: true },
from: { after: true },
import: { after: true },
export: { after: true },
default: { after: true },
// The new option:
type: { after: true },
},
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
// Space after import is not configurable from option since it's invalid syntax with import type
code: 'import type { SavedQueries } from "./SavedQueries.js";',
options: [
{
before: true,
after: true,
overrides: {
import: { after: false },
},
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: "import type{SavedQueries} from './SavedQueries.js';",
options: [
{
before: true,
after: false,
overrides: {
from: { after: true },
},
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: "import type{SavedQueries} from'./SavedQueries.js';",
options: [
{
before: true,
after: false,
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: "import type http from 'node:http';",
options: [
{
before: true,
after: false,
overrides: {
from: { after: true },
},
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: "import type http from'node:http';",
options: [
{
before: true,
after: false,
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'import type {} from "foo";',
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'import type { foo1, foo2 } from "foo";',
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'import type { foo1 as _foo1, foo2 as _foo2 } from "foo";',
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
{
code: 'import type { foo as bar } from "foo";',
options: [BOTH],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
},
],
Expand Down Expand Up @@ -167,28 +277,58 @@ ruleTester.run('keyword-spacing', rule, {
output: 'import type { foo } from "foo";',
options: [{ after: true, before: true }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: [{ messageId: 'expectedBefore', data: { value: '{' } }],
errors: expectedAfter('type'),
},
{
code: 'import type { foo } from"foo";',
output: 'import type{ foo } from"foo";',
options: [{ after: false, before: true }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: [{ messageId: 'unexpectedBefore', data: { value: '{' } }],
errors: unexpectedAfter('type'),
},
{
code: 'import type* as foo from "foo";',
output: 'import type * as foo from "foo";',
options: [{ after: true, before: true }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: [{ messageId: 'expectedBefore', data: { value: '*' } }],
errors: expectedAfter('type'),
},
{
code: 'import type * as foo from"foo";',
output: 'import type* as foo from"foo";',
options: [{ after: false, before: true }],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: [{ messageId: 'unexpectedBefore', data: { value: '*' } }],
errors: unexpectedAfter('type'),
},
{
code: "import type {SavedQueries} from './SavedQueries.js';",
output: "import type{SavedQueries} from './SavedQueries.js';",
options: [
{
before: true,
after: false,
overrides: {
from: { after: true },
},
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: unexpectedAfter('type'),
},
{
code: "import type {SavedQueries} from './SavedQueries.js';",
output: "import type{SavedQueries} from'./SavedQueries.js';",
options: [
{
before: true,
after: false,
},
],
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
errors: [
{ messageId: 'unexpectedAfter', data: { value: 'type' } },
{ messageId: 'unexpectedAfter', data: { value: 'from' } },
],
},
],
});

0 comments on commit 98caa92

Please sign in to comment.