From 304f7e3da4fb7a4c38eff0fa27cc6db417bfe10c Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 11 Apr 2022 21:47:26 +1200 Subject: [PATCH] Migrate `@emotion/eslint-plugin` to TypeScript (#2568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(eslint-plugin): add standard utils & dependencies for typescript * feat(eslint-plugin): convert `no-vanilla` to typescript * feat(eslint-plugin): convert `styled-import` to typescript * feat(eslint-plugin): convert `import-from-emotion` to typescript * feat(eslint-plugin): convert `syntax-preference` to typescript * feat(eslint-plugin): convert `pkg-renaming` to typescript * feat(eslint-plugin): convert `jsx-import` to typescript * feat(eslint-plugin): move index to typescript * feat(eslint-plugin): add changeset * fix(eslint-plugin): use `require` to get package version * fix(eslint-plugin): adjust rule description Co-authored-by: Sam Magura * fix(eslint-plugin): use `REPO_URL` constant everywhere * chore(eslint-plugin): add change sets for other bugs * nit * Tweak changeset * Report empty css attribute as invalid in syntax-preference rule * Tweak changesets Co-authored-by: Sam Magura Co-authored-by: Mateusz BurzyƄski --- .changeset/lovely-zebras-admire.md | 5 + .changeset/modern-penguins-play.md | 5 + .changeset/sharp-trees-smell.md | 5 + packages/eslint-plugin/package.json | 7 +- .../eslint-plugin/src/{index.js => index.ts} | 0 .../src/rules/import-from-emotion.js | 42 ------ .../src/rules/import-from-emotion.ts | 68 +++++++++ .../rules/{jsx-import.js => jsx-import.ts} | 130 +++++++++++++----- .../eslint-plugin/src/rules/no-vanilla.js | 17 --- .../eslint-plugin/src/rules/no-vanilla.ts | 30 ++++ .../eslint-plugin/src/rules/pkg-renaming.js | 67 --------- .../eslint-plugin/src/rules/pkg-renaming.ts | 88 ++++++++++++ .../eslint-plugin/src/rules/styled-import.js | 25 ---- .../eslint-plugin/src/rules/styled-import.ts | 40 ++++++ ...tax-preference.js => syntax-preference.ts} | 129 ++++++++++++----- packages/eslint-plugin/src/utils.ts | 12 ++ ...on.test.js => import-from-emotion.test.ts} | 21 ++- ...{jsx-import.test.js => jsx-import.test.ts} | 86 +++++++----- ...{no-vanilla.test.js => no-vanilla.test.ts} | 14 +- ...-renaming.test.js => pkg-renaming.test.ts} | 41 ++++-- ...d-import.test.js => styled-import.test.ts} | 14 +- ...ence.test.js => syntax-preference.test.ts} | 100 ++++++++------ packages/eslint-plugin/test/test-utils.ts | 6 + tsconfig.json | 1 + yarn.lock | 10 +- 25 files changed, 616 insertions(+), 347 deletions(-) create mode 100644 .changeset/lovely-zebras-admire.md create mode 100644 .changeset/modern-penguins-play.md create mode 100644 .changeset/sharp-trees-smell.md rename packages/eslint-plugin/src/{index.js => index.ts} (100%) delete mode 100644 packages/eslint-plugin/src/rules/import-from-emotion.js create mode 100644 packages/eslint-plugin/src/rules/import-from-emotion.ts rename packages/eslint-plugin/src/rules/{jsx-import.js => jsx-import.ts} (57%) delete mode 100644 packages/eslint-plugin/src/rules/no-vanilla.js create mode 100644 packages/eslint-plugin/src/rules/no-vanilla.ts delete mode 100644 packages/eslint-plugin/src/rules/pkg-renaming.js create mode 100644 packages/eslint-plugin/src/rules/pkg-renaming.ts delete mode 100644 packages/eslint-plugin/src/rules/styled-import.js create mode 100644 packages/eslint-plugin/src/rules/styled-import.ts rename packages/eslint-plugin/src/rules/{syntax-preference.js => syntax-preference.ts} (55%) create mode 100644 packages/eslint-plugin/src/utils.ts rename packages/eslint-plugin/test/rules/{import-from-emotion.test.js => import-from-emotion.test.ts} (60%) rename packages/eslint-plugin/test/rules/{jsx-import.test.js => jsx-import.test.ts} (71%) rename packages/eslint-plugin/test/rules/{no-vanilla.test.js => no-vanilla.test.ts} (59%) rename packages/eslint-plugin/test/rules/{pkg-renaming.test.js => pkg-renaming.test.ts} (53%) rename packages/eslint-plugin/test/rules/{styled-import.test.js => styled-import.test.ts} (64%) rename packages/eslint-plugin/test/rules/{syntax-preference.test.js => syntax-preference.test.ts} (68%) create mode 100644 packages/eslint-plugin/test/test-utils.ts diff --git a/.changeset/lovely-zebras-admire.md b/.changeset/lovely-zebras-admire.md new file mode 100644 index 000000000..69c62f845 --- /dev/null +++ b/.changeset/lovely-zebras-admire.md @@ -0,0 +1,5 @@ +--- +'@emotion/eslint-plugin': patch +--- + +An empty css prop (`
`) will now raise an error in the `@emotion/syntax-preference` rule instead of crashing on this case. diff --git a/.changeset/modern-penguins-play.md b/.changeset/modern-penguins-play.md new file mode 100644 index 000000000..816b0c5a5 --- /dev/null +++ b/.changeset/modern-penguins-play.md @@ -0,0 +1,5 @@ +--- +'@emotion/eslint-plugin': minor +--- + +Source code has been migrated to TypeScript. From now on type declarations will be emitted based on that, instead of being hand-written. diff --git a/.changeset/sharp-trees-smell.md b/.changeset/sharp-trees-smell.md new file mode 100644 index 000000000..2a27ad044 --- /dev/null +++ b/.changeset/sharp-trees-smell.md @@ -0,0 +1,5 @@ +--- +'@emotion/eslint-plugin': patch +--- + +Fixed a crash on empty css prop (`
`) in the `@emotion/jsx-import` rule. diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c6c399666..3759eda74 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -21,7 +21,12 @@ "peerDependencies": { "eslint": "6 || 7 || 8" }, + "dependencies": { + "@typescript-eslint/experimental-utils": "^4.30.0" + }, "devDependencies": { - "eslint": "^7.10.0" + "@types/eslint": "^7.0.0", + "eslint": "^7.10.0", + "resolve-from": "^5.0.0" } } diff --git a/packages/eslint-plugin/src/index.js b/packages/eslint-plugin/src/index.ts similarity index 100% rename from packages/eslint-plugin/src/index.js rename to packages/eslint-plugin/src/index.ts diff --git a/packages/eslint-plugin/src/rules/import-from-emotion.js b/packages/eslint-plugin/src/rules/import-from-emotion.js deleted file mode 100644 index 7889e3ca5..000000000 --- a/packages/eslint-plugin/src/rules/import-from-emotion.js +++ /dev/null @@ -1,42 +0,0 @@ -export default { - meta: { - fixable: 'code' - }, - create(context) { - return { - ImportDeclaration(node) { - if ( - node.source.value === 'react-emotion' && - node.specifiers.some(x => x.type !== 'ImportDefaultSpecifier') - ) { - context.report({ - node: node.source, - message: `emotion's exports should be imported directly from emotion rather than from react-emotion`, - fix(fixer) { - if (node.specifiers[0].type === 'ImportNamespaceSpecifier') { - return - } - // default specifiers are always first - if (node.specifiers[0].type === 'ImportDefaultSpecifier') { - return fixer.replaceText( - node, - `import ${ - node.specifiers[0].local.name - } from '@emotion/styled';\nimport { ${node.specifiers - .filter(x => x.type === 'ImportSpecifier') - .map(x => - x.local.name === x.imported.name - ? x.local.name - : `${x.imported.name} as ${x.local.name}` - ) - .join(', ')} } from 'emotion';` - ) - } - return fixer.replaceText(node.source, "'emotion'") - } - }) - } - } - } - } -} diff --git a/packages/eslint-plugin/src/rules/import-from-emotion.ts b/packages/eslint-plugin/src/rules/import-from-emotion.ts new file mode 100644 index 000000000..a936a0a7d --- /dev/null +++ b/packages/eslint-plugin/src/rules/import-from-emotion.ts @@ -0,0 +1,68 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/experimental-utils' +import { createRule } from '../utils' + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Ensure styled is imported from @emotion/styled', + recommended: false + }, + fixable: 'code', + messages: { + incorrectImport: `emotion's exports should be imported directly from emotion rather than from react-emotion` + }, + schema: [], + type: 'problem' + }, + defaultOptions: [], + create(context) { + return { + ImportDeclaration(node) { + if ( + node.source.value === 'react-emotion' && + node.specifiers.some( + x => x.type !== AST_NODE_TYPES.ImportDefaultSpecifier + ) + ) { + context.report({ + node: node.source, + messageId: 'incorrectImport', + fix(fixer) { + if ( + node.specifiers[0].type === + AST_NODE_TYPES.ImportNamespaceSpecifier + ) { + return null + } + // default specifiers are always first + if ( + node.specifiers[0].type === + AST_NODE_TYPES.ImportDefaultSpecifier + ) { + return fixer.replaceText( + node, + `import ${ + node.specifiers[0].local.name + } from '@emotion/styled';\nimport { ${node.specifiers + .filter( + (x): x is TSESTree.ImportSpecifier => + x.type === AST_NODE_TYPES.ImportSpecifier + ) + .map(x => + x.local.name === x.imported.name + ? x.local.name + : `${x.imported.name} as ${x.local.name}` + ) + .join(', ')} } from 'emotion';` + ) + } + return fixer.replaceText(node.source, "'emotion'") + } + }) + } + } + } + } +}) diff --git a/packages/eslint-plugin/src/rules/jsx-import.js b/packages/eslint-plugin/src/rules/jsx-import.ts similarity index 57% rename from packages/eslint-plugin/src/rules/jsx-import.js rename to packages/eslint-plugin/src/rules/jsx-import.ts index 8b7cab8a6..b6cda8955 100644 --- a/packages/eslint-plugin/src/rules/jsx-import.js +++ b/packages/eslint-plugin/src/rules/jsx-import.ts @@ -1,3 +1,6 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/experimental-utils' +import { createRule, REPO_URL } from '../utils' + const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/ const JSX_IMPORT_SOURCE_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/ @@ -6,9 +9,35 @@ const JSX_IMPORT_SOURCE_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/ // to //
+ import { css } -export default { +declare module '@typescript-eslint/experimental-utils/dist/ts-eslint/Rule' { + export interface SharedConfigurationSettings { + react?: { pragma?: string } + } +} + +interface JSXConfig { + runtime: string + importSource?: string +} + +type RuleOptions = [(JSXConfig | string)?] + +const messages = { + cssProp: `The css prop can only be used if jsxImportSource is set to {{ importSource }}`, + cssPropWithPragma: `The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma`, + templateLiterals: `Template literals should be replaced with tagged template literals using \`css\` when using the css prop` +} + +export default createRule({ + name: __filename, meta: { + docs: { + category: 'Possible Errors', + description: 'Ensure jsx from @emotion/react is imported', + recommended: false + }, fixable: 'code', + messages, schema: { type: 'array', items: { @@ -29,11 +58,14 @@ export default { }, uniqueItems: true, minItems: 0 - } + }, + type: 'problem' }, + defaultOptions: [], create(context) { const jsxRuntimeMode = context.options.find( - option => option && option.runtime === 'automatic' + (option): option is JSXConfig => + typeof option === 'object' && option.runtime === 'automatic' ) if (jsxRuntimeMode) { @@ -42,15 +74,14 @@ export default { if (node.name.name !== 'css') { return } - const importSource = - (jsxRuntimeMode || {}).importSource || '@emotion/react' - let jsxImportSourcePragmaNode + const importSource = jsxRuntimeMode?.importSource || '@emotion/react' + let jsxImportSourcePragmaComment: TSESTree.Comment | null = null let jsxImportSourceMatch let validJsxImportSource = false let sourceCode = context.getSourceCode() - let pragma = sourceCode.getAllComments().find(node => { - if (JSX_IMPORT_SOURCE_REGEX.test(node.value)) { - jsxImportSourcePragmaNode = node + let pragma = sourceCode.getAllComments().find(comment => { + if (JSX_IMPORT_SOURCE_REGEX.test(comment.value)) { + jsxImportSourcePragmaComment = comment return true } }) @@ -65,7 +96,8 @@ export default { if (!jsxImportSourceMatch) { context.report({ node, - message: `The css prop can only be used if jsxImportSource is set to ${importSource}`, + messageId: 'cssProp', + data: { importSource }, fix(fixer) { return fixer.insertTextBefore( sourceCode.ast.body[0], @@ -73,13 +105,21 @@ export default { ) } }) - } else if (!validJsxImportSource && jsxImportSourcePragmaNode) { + } else if (!validJsxImportSource && jsxImportSourcePragmaComment) { context.report({ node, - message: `The css prop can only be used if jsxImportSource is set to ${importSource}`, + messageId: 'cssProp', + data: { importSource }, fix(fixer) { + /* istanbul ignore if */ + if (jsxImportSourcePragmaComment === null) { + throw new Error( + `Unexpected null when attempting to fix ${context.getFilename()} - please file a github issue at ${REPO_URL}` + ) + } + return fixer.replaceText( - jsxImportSourcePragmaNode, + jsxImportSourcePragmaComment, `/** @jsxImportSource ${importSource} */` ) } @@ -95,12 +135,12 @@ export default { return } let hasJsxImport = false - let emotionCoreNode = null - let local = null + let emotionCoreNode = null as TSESTree.ImportDeclaration | null + let local: string | null = null let sourceCode = context.getSourceCode() sourceCode.ast.body.forEach(x => { if ( - x.type === 'ImportDeclaration' && + x.type === AST_NODE_TYPES.ImportDeclaration && (x.source.value === '@emotion/react' || x.source.value === '@emotion/core') ) { @@ -108,13 +148,15 @@ export default { if ( x.specifiers.length === 1 && - x.specifiers[0].type === 'ImportNamespaceSpecifier' + x.specifiers[0].type === AST_NODE_TYPES.ImportNamespaceSpecifier ) { hasJsxImport = true local = x.specifiers[0].local.name + '.jsx' } else { let jsxSpecifier = x.specifiers.find( - x => x.type === 'ImportSpecifier' && x.imported.name === 'jsx' + x => + x.type === AST_NODE_TYPES.ImportSpecifier && + x.imported.name === 'jsx' ) if (jsxSpecifier) { hasJsxImport = true @@ -138,10 +180,16 @@ export default { if (!hasJsxImport || !hasSetPragma) { context.report({ node, - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma', + messageId: 'cssPropWithPragma', fix(fixer) { if (hasJsxImport) { + /* istanbul ignore if */ + if (emotionCoreNode === null) { + throw new Error( + `Unexpected null when attempting to fix ${context.getFilename()} - please file a github issue at ${REPO_URL}` + ) + } + return fixer.insertTextBefore( emotionCoreNode, `/** @jsx ${local} */\n` @@ -154,7 +202,9 @@ export default { emotionCoreNode.specifiers.length - 1 ] - if (lastSpecifier.type === 'ImportDefaultSpecifier') { + if ( + lastSpecifier.type === AST_NODE_TYPES.ImportDefaultSpecifier + ) { return fixer.insertTextAfter(lastSpecifier, ', { jsx }') } @@ -174,38 +224,48 @@ export default { }) return } + + /* istanbul ignore if */ + if (emotionCoreNode === null) { + throw new Error( + `Unexpected null when attempting to fix ${context.getFilename()} - please file a github issue at ${REPO_URL}` + ) + } + + const { specifiers } = emotionCoreNode + const { value } = node + if ( - node.value.type === 'JSXExpressionContainer' && - node.value.expression.type === 'TemplateLiteral' + value && + value.type === AST_NODE_TYPES.JSXExpressionContainer && + value.expression.type === AST_NODE_TYPES.TemplateLiteral ) { - let cssSpecifier = emotionCoreNode.specifiers.find( - x => x.imported.name === 'css' + let cssSpecifier = specifiers.find( + x => + x.type === AST_NODE_TYPES.ImportSpecifier && + x.imported.name === 'css' ) context.report({ node, - message: - 'Template literals should be replaced with tagged template literals using `css` when using the css prop', + messageId: 'templateLiterals', fix(fixer) { if (cssSpecifier) { return fixer.insertTextBefore( - node.value.expression, + value.expression, cssSpecifier.local.name ) } - let lastSpecifier = - emotionCoreNode.specifiers[ - emotionCoreNode.specifiers.length - 1 - ] + let lastSpecifier = specifiers[specifiers.length - 1] if (context.getScope().variables.some(x => x.name === 'css')) { return [ fixer.insertTextAfter(lastSpecifier, `, css as _css`), - fixer.insertTextBefore(node.value.expression, '_css') + fixer.insertTextBefore(value.expression, '_css') ] } return [ fixer.insertTextAfter(lastSpecifier, `, css`), - fixer.insertTextBefore(node.value.expression, 'css') + fixer.insertTextBefore(value.expression, 'css') ] } }) @@ -213,4 +273,4 @@ export default { } } } -} +}) diff --git a/packages/eslint-plugin/src/rules/no-vanilla.js b/packages/eslint-plugin/src/rules/no-vanilla.js deleted file mode 100644 index 793819576..000000000 --- a/packages/eslint-plugin/src/rules/no-vanilla.js +++ /dev/null @@ -1,17 +0,0 @@ -export default { - meta: { - fixable: 'code' - }, - create(context) { - return { - ImportDeclaration(node) { - if (node.source.value === '@emotion/css') { - context.report({ - node: node.source, - message: `Vanilla emotion should not be used` - }) - } - } - } - } -} diff --git a/packages/eslint-plugin/src/rules/no-vanilla.ts b/packages/eslint-plugin/src/rules/no-vanilla.ts new file mode 100644 index 000000000..3ded805ef --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-vanilla.ts @@ -0,0 +1,30 @@ +import { createRule } from '../utils' + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Ensure vanilla emotion is not used', + recommended: false + }, + messages: { + vanillaEmotion: 'Vanilla emotion should not be used' + }, + schema: [], + type: 'problem' + }, + defaultOptions: [], + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value === '@emotion/css') { + context.report({ + node: node.source, + messageId: 'vanillaEmotion' + }) + } + } + } + } +}) diff --git a/packages/eslint-plugin/src/rules/pkg-renaming.js b/packages/eslint-plugin/src/rules/pkg-renaming.js deleted file mode 100644 index 2e6dbfaf8..000000000 --- a/packages/eslint-plugin/src/rules/pkg-renaming.js +++ /dev/null @@ -1,67 +0,0 @@ -let simpleMappings = { - '@emotion/core': '@emotion/react', - emotion: '@emotion/css', - 'emotion/macro': '@emotion/css/macro', - '@emotion/styled-base': '@emotion/styled/base', - 'jest-emotion': '@emotion/jest', - 'babel-plugin-emotion': '@emotion/babel-plugin', - 'eslint-plugin-emotion': '@emotion/eslint-plugin', - 'create-emotion-server': '@emotion/server/create-instance', - 'create-emotion': '@emotion/css/create-instance', - 'emotion-server': '@emotion/server' -} - -export default { - meta: { - fixable: 'code' - }, - create(context) { - return { - ImportDeclaration(node) { - let maybeMapping = simpleMappings[node.source.value] - if (maybeMapping !== undefined) { - context.report({ - node: node.source, - message: `${JSON.stringify( - node.source.value - )} has been renamed to ${JSON.stringify( - maybeMapping - )}, please import it from ${JSON.stringify(maybeMapping)} instead`, - fix: fixer => fixer.replaceText(node.source, `'${maybeMapping}'`) - }) - } - if ( - (node.source.value === '@emotion/css' || - node.source.value === '@emotion/css/macro') && - node.specifiers.length === 1 && - node.specifiers[0].type === 'ImportDefaultSpecifier' - ) { - let replacement = - node.source.value === '@emotion/css' - ? '@emotion/react' - : '@emotion/react/macro' - context.report({ - node: node.source, - message: `The default export of "${node.source.value}" in Emotion 10 has been moved to a named export, \`css\`, from "${replacement}" in Emotion 11, please import it from "${replacement}"`, - fix: fixer => - fixer.replaceText( - node, - `import { css${ - node.specifiers[0].local.name === 'css' - ? '' - : ` as ${node.specifiers[0].local.name}` - } } from '${replacement}'` - ) - }) - } - if (node.source.value === 'emotion-theming') { - context.report({ - node: node.source, - message: `"emotion-theming" has been moved into "@emotion/react", please import its exports from "@emotion/react"`, - fix: fixer => fixer.replaceText(node.source, `'@emotion/react'`) - }) - } - } - } - } -} diff --git a/packages/eslint-plugin/src/rules/pkg-renaming.ts b/packages/eslint-plugin/src/rules/pkg-renaming.ts new file mode 100644 index 000000000..7a412b050 --- /dev/null +++ b/packages/eslint-plugin/src/rules/pkg-renaming.ts @@ -0,0 +1,88 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils' +import { createRule } from '../utils' + +const simpleMappings = new Map([ + ['@emotion/core', '@emotion/react'], + ['emotion', '@emotion/css'], + ['emotion/macro', '@emotion/css/macro'], + ['@emotion/styled-base', '@emotion/styled/base'], + ['jest-emotion', '@emotion/jest'], + ['babel-plugin-emotion', '@emotion/babel-plugin'], + ['eslint-plugin-emotion', '@emotion/eslint-plugin'], + ['create-emotion-server', '@emotion/server/create-instance'], + ['create-emotion', '@emotion/css/create-instance'], + ['emotion-server', '@emotion/server'] +]) + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Internal rule', + recommended: false + }, + fixable: 'code', + messages: { + renamePackage: `{{ beforeName }} has been renamed to {{ afterName }}, please import it from {{ afterName }} instead`, + exportChange: `The default export of "{{ name }}" in Emotion 10 has been moved to a named export, \`css\`, from "{{ replacement }}" in Emotion 11, please import it from "{{ replacement }}"`, + emotionTheming: `"emotion-theming" has been moved into "@emotion/react", please import its exports from "@emotion/react"` + }, + schema: [], + type: 'problem' + }, + defaultOptions: [], + create(context) { + return { + ImportDeclaration(node) { + const maybeMapping = simpleMappings.get(node.source.value) + if (maybeMapping !== undefined) { + context.report({ + node: node.source, + messageId: 'renamePackage', + data: { + beforeName: JSON.stringify(node.source.value), + afterName: JSON.stringify(maybeMapping) + }, + fix: fixer => fixer.replaceText(node.source, `'${maybeMapping}'`) + }) + } + if ( + (node.source.value === '@emotion/css' || + node.source.value === '@emotion/css/macro') && + node.specifiers.length === 1 && + node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier + ) { + let replacement = + node.source.value === '@emotion/css' + ? '@emotion/react' + : '@emotion/react/macro' + context.report({ + node: node.source, + messageId: 'exportChange', + data: { + name: node.source.value, + replacement + }, + fix: fixer => + fixer.replaceText( + node, + `import { css${ + node.specifiers[0].local.name === 'css' + ? '' + : ` as ${node.specifiers[0].local.name}` + } } from '${replacement}'` + ) + }) + } + if (node.source.value === 'emotion-theming') { + context.report({ + node: node.source, + messageId: 'emotionTheming', + fix: fixer => fixer.replaceText(node.source, `'@emotion/react'`) + }) + } + } + } + } +}) diff --git a/packages/eslint-plugin/src/rules/styled-import.js b/packages/eslint-plugin/src/rules/styled-import.js deleted file mode 100644 index 89ccf2310..000000000 --- a/packages/eslint-plugin/src/rules/styled-import.js +++ /dev/null @@ -1,25 +0,0 @@ -export default { - meta: { - fixable: 'code' - }, - create(context) { - return { - ImportDeclaration(node) { - if (node.source.value === 'react-emotion') { - let newImportPath = '@emotion/styled' - context.report({ - node: node.source, - message: `styled should be imported from @emotion/styled`, - fix: - node.specifiers.length === 1 && - node.specifiers[0].type === 'ImportDefaultSpecifier' - ? fixer => { - return fixer.replaceText(node.source, `'${newImportPath}'`) - } - : undefined - }) - } - } - } - } -} diff --git a/packages/eslint-plugin/src/rules/styled-import.ts b/packages/eslint-plugin/src/rules/styled-import.ts new file mode 100644 index 000000000..c0d7e99ab --- /dev/null +++ b/packages/eslint-plugin/src/rules/styled-import.ts @@ -0,0 +1,40 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils' +import { createRule } from '../utils' + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Ensure styled is imported from @emotion/styled', + recommended: false + }, + fixable: 'code', + messages: { + incorrectImport: 'styled should be imported from @emotion/styled' + }, + schema: [], + type: 'problem' + }, + defaultOptions: [], + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value === 'react-emotion') { + let newImportPath = '@emotion/styled' + context.report({ + node: node.source, + messageId: 'incorrectImport', + fix: + node.specifiers.length === 1 && + node.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier + ? fixer => { + return fixer.replaceText(node.source, `'${newImportPath}'`) + } + : undefined + }) + } + } + } + } +}) diff --git a/packages/eslint-plugin/src/rules/syntax-preference.js b/packages/eslint-plugin/src/rules/syntax-preference.ts similarity index 55% rename from packages/eslint-plugin/src/rules/syntax-preference.js rename to packages/eslint-plugin/src/rules/syntax-preference.ts index 891713a87..df0622d5f 100644 --- a/packages/eslint-plugin/src/rules/syntax-preference.js +++ b/packages/eslint-plugin/src/rules/syntax-preference.ts @@ -1,16 +1,24 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree +} from '@typescript-eslint/experimental-utils' +import { createRule } from '../utils' + /** * @fileoverview Choose between string or object syntax * @author alex-pex */ -function isStringStyle(node) { - if (node.tag.type === 'Identifier' && node.tag.name === 'css') { +function isStringStyle(node: TSESTree.TaggedTemplateExpression) { + if (node.tag.type === AST_NODE_TYPES.Identifier && node.tag.name === 'css') { return true } // shorthand notation // eg: styled.h1` color: red; ` if ( - node.tag.type === 'MemberExpression' && + node.tag.type === AST_NODE_TYPES.MemberExpression && + node.tag.object.type === AST_NODE_TYPES.Identifier && node.tag.object.name === 'styled' ) { // string syntax used @@ -19,7 +27,11 @@ function isStringStyle(node) { // full notation // eg: styled('h1')` color: red; ` - if (node.tag.type === 'CallExpression' && node.tag.callee.name === 'styled') { + if ( + node.tag.type === AST_NODE_TYPES.CallExpression && + node.tag.callee.type === AST_NODE_TYPES.Identifier && + node.tag.callee.name === 'styled' + ) { // string syntax used return true } @@ -27,15 +39,19 @@ function isStringStyle(node) { return false } -function isObjectStyle(node) { - if (node.callee.type === 'Identifier' && node.callee.name === 'css') { +function isObjectStyle(node: TSESTree.CallExpression) { + if ( + node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === 'css' + ) { return true } // shorthand notation // eg: styled.h1({ color: 'red' }) if ( - node.callee.type === 'MemberExpression' && + node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && node.callee.object.name === 'styled' ) { // object syntax used @@ -45,7 +61,8 @@ function isObjectStyle(node) { // full notation // eg: styled('h1')({ color: 'red' }) if ( - node.callee.type === 'CallExpression' && + node.callee.type === AST_NODE_TYPES.CallExpression && + node.callee.callee.type === AST_NODE_TYPES.Identifier && node.callee.callee.name === 'styled' ) { // object syntax used @@ -59,42 +76,42 @@ function isObjectStyle(node) { // Rule Definition // ------------------------------------------------------------------------------ -const MSG_PREFER_STRING_STYLE = 'Styles should be written using strings.' -const MSG_PREFER_OBJECT_STYLE = 'Styles should be written using objects.' -const MSG_PREFER_WRAPPING_WITH_CSS = - 'Prefer wrapping your string styles with `css` call.' - -const checkExpressionPreferringObject = (context, node) => { +const checkExpressionPreferringObject = ( + context: RuleContext, + node: TSESTree.Node +) => { switch (node.type) { - case 'ArrayExpression': + case AST_NODE_TYPES.ArrayExpression: node.elements.forEach(element => checkExpressionPreferringObject(context, element) ) return - case 'TemplateLiteral': + case AST_NODE_TYPES.TemplateLiteral: context.report({ node, - message: MSG_PREFER_OBJECT_STYLE + messageId: 'preferObjectStyle' }) return - case 'Literal': + case AST_NODE_TYPES.Literal: // validating other literal types seems out of scope of this plugin if (typeof node.value !== 'string') { return } context.report({ node, - message: MSG_PREFER_OBJECT_STYLE + messageId: 'preferObjectStyle' }) } } -const createPreferredObjectVisitor = context => ({ +const createPreferredObjectVisitor = ( + context: RuleContext +): TSESLint.RuleListener => ({ TaggedTemplateExpression(node) { if (isStringStyle(node)) { context.report({ node, - message: MSG_PREFER_OBJECT_STYLE + messageId: 'preferObjectStyle' }) } }, @@ -110,24 +127,35 @@ const createPreferredObjectVisitor = context => ({ return } + if (!node.value) { + context.report({ + node: node, + messageId: 'emptyCssProp' + }) + return + } + switch (node.value.type) { - case 'Literal': + case AST_NODE_TYPES.Literal: // validating other literal types seems out of scope of this plugin if (typeof node.value.value !== 'string') { return } context.report({ node: node.value, - message: MSG_PREFER_OBJECT_STYLE + messageId: 'preferObjectStyle' }) return - case 'JSXExpressionContainer': + case AST_NODE_TYPES.JSXExpressionContainer: checkExpressionPreferringObject(context, node.value.expression) } } }) -const checkExpressionPreferringString = (context, node) => { +const checkExpressionPreferringString = ( + context: RuleContext, + node: TSESTree.Node +) => { switch (node.type) { case 'ArrayExpression': node.elements.forEach(element => @@ -137,7 +165,7 @@ const checkExpressionPreferringString = (context, node) => { case 'ObjectExpression': context.report({ node, - message: MSG_PREFER_STRING_STYLE + messageId: 'preferStringStyle' }) return case 'Literal': @@ -147,12 +175,14 @@ const checkExpressionPreferringString = (context, node) => { } context.report({ node, - message: MSG_PREFER_WRAPPING_WITH_CSS + messageId: 'preferWrappingWithCSS' }) } } -const createPreferredStringVisitor = context => ({ +const createPreferredStringVisitor = ( + context: RuleContext +): TSESLint.RuleListener => ({ CallExpression(node) { if (isObjectStyle(node)) { node.arguments.forEach(argument => @@ -166,38 +196,63 @@ const createPreferredStringVisitor = context => ({ return } + if (!node.value) { + context.report({ + node: node, + messageId: 'emptyCssProp' + }) + return + } + switch (node.value.type) { - case 'Literal': + case AST_NODE_TYPES.Literal: // validating other literal types seems out of scope of this plugin if (typeof node.value.value !== 'string') { return } context.report({ node: node.value, - message: MSG_PREFER_WRAPPING_WITH_CSS + messageId: 'preferWrappingWithCSS' }) return - case 'JSXExpressionContainer': + case AST_NODE_TYPES.JSXExpressionContainer: checkExpressionPreferringString(context, node.value.expression) } } }) -export default { +type RuleOptions = [('string' | 'object')?] + +type MessageId = + | 'preferStringStyle' + | 'preferObjectStyle' + | 'preferWrappingWithCSS' + | 'emptyCssProp' + +type RuleContext = TSESLint.RuleContext + +export default createRule({ + name: __filename, meta: { docs: { - description: 'Choose between string or object styles', category: 'Stylistic Issues', + description: 'Choose between styles written as strings or objects', recommended: false }, - fixable: null, // or "code" or "whitespace" + messages: { + preferStringStyle: 'Styles should be written using strings.', + preferObjectStyle: 'Styles should be written using objects.', + preferWrappingWithCSS: `Prefer wrapping your string styles with \`css\` call.`, + emptyCssProp: `Empty \`css\` prop is not valid.` + }, schema: [ { enum: ['string', 'object'] } - ] + ], + type: 'problem' }, - + defaultOptions: [], create(context) { const preferredSyntax = context.options[0] @@ -210,4 +265,4 @@ export default { return {} } } -} +}) diff --git a/packages/eslint-plugin/src/utils.ts b/packages/eslint-plugin/src/utils.ts new file mode 100644 index 000000000..268d163b4 --- /dev/null +++ b/packages/eslint-plugin/src/utils.ts @@ -0,0 +1,12 @@ +import { ESLintUtils } from '@typescript-eslint/experimental-utils' +import { parse as parsePath } from 'path' + +const { version } = require('../package.json') + +export const REPO_URL = 'https://github.com/emotion-js/emotion' + +export const createRule = ESLintUtils.RuleCreator(name => { + const ruleName = parsePath(name).name + + return `${REPO_URL}/blob/@emotion/eslint-plugin@${version}/packages/eslint-plugin/docs/rules/${ruleName}.md` +}) diff --git a/packages/eslint-plugin/test/rules/import-from-emotion.test.js b/packages/eslint-plugin/test/rules/import-from-emotion.test.ts similarity index 60% rename from packages/eslint-plugin/test/rules/import-from-emotion.test.js rename to packages/eslint-plugin/test/rules/import-from-emotion.test.ts index b28f99730..c39c8701e 100644 --- a/packages/eslint-plugin/test/rules/import-from-emotion.test.js +++ b/packages/eslint-plugin/test/rules/import-from-emotion.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ -import { RuleTester } from 'eslint' -import { rules as emotionRules } from '@emotion/eslint-plugin' +import { TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/import-from-emotion' +import { espreeParser } from '../test-utils' -const rule = emotionRules['import-from-emotion'] - -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -17,8 +17,6 @@ RuleTester.setDefaultConfig({ } }) -const ruleTester = new RuleTester() - ruleTester.run('emotion jsx', rule, { valid: [ { @@ -31,8 +29,7 @@ ruleTester.run('emotion jsx', rule, { code: `import { css } from 'react-emotion'`, errors: [ { - message: - "emotion's exports should be imported directly from emotion rather than from react-emotion" + messageId: 'incorrectImport' } ], output: `import { css } from 'emotion'` @@ -41,8 +38,7 @@ ruleTester.run('emotion jsx', rule, { code: `import styled, { css } from 'react-emotion'`, errors: [ { - message: - "emotion's exports should be imported directly from emotion rather than from react-emotion" + messageId: 'incorrectImport' } ], output: `import styled from '@emotion/styled';\nimport { css } from 'emotion';` @@ -51,8 +47,7 @@ ruleTester.run('emotion jsx', rule, { code: `import styled, { css as somethingElse } from 'react-emotion'`, errors: [ { - message: - "emotion's exports should be imported directly from emotion rather than from react-emotion" + messageId: 'incorrectImport' } ], output: `import styled from '@emotion/styled';\nimport { css as somethingElse } from 'emotion';` diff --git a/packages/eslint-plugin/test/rules/jsx-import.test.js b/packages/eslint-plugin/test/rules/jsx-import.test.ts similarity index 71% rename from packages/eslint-plugin/test/rules/jsx-import.test.js rename to packages/eslint-plugin/test/rules/jsx-import.test.ts index 6a0fa0b79..f04912845 100644 --- a/packages/eslint-plugin/test/rules/jsx-import.test.js +++ b/packages/eslint-plugin/test/rules/jsx-import.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ -import { RuleTester } from 'eslint' -import { rules as emotionRules } from '@emotion/eslint-plugin' +import { TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/jsx-import' +import { espreeParser } from '../test-utils' -const rule = emotionRules['jsx-import'] - -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -17,8 +17,6 @@ RuleTester.setDefaultConfig({ } }) -const ruleTester = new RuleTester() - ruleTester.run('emotion jsx', rule, { valid: [ { @@ -73,6 +71,14 @@ ruleTester.run('emotion jsx', rule, { let ele =
` + }, + { + code: ` + /** @jsx jsx */ + import {jsx} from '@emotion/react' + // it's invalid but not for this rule + let ele =
+ ` } ], @@ -84,8 +90,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -101,8 +106,8 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsxImportSource is set to @emotion/react' + messageId: 'cssProp', + data: { importSource: '@emotion/react' } } ], output: ` @@ -117,8 +122,8 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsxImportSource is set to @iChenLei/react' + messageId: 'cssProp', + data: { importSource: '@iChenLei/react' } } ], output: ` @@ -134,8 +139,8 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsxImportSource is set to @iChenLei/react' + messageId: 'cssProp', + data: { importSource: '@iChenLei/react' } } ], output: ` @@ -150,8 +155,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -168,8 +172,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -186,8 +189,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -203,8 +205,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -219,8 +220,7 @@ let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -231,13 +231,30 @@ let ele =
}, { code: ` +/** @jsx jsx */ +import * as emotion from '@emotion/react' +let ele =
+ `.trim(), + errors: [ + { + messageId: 'cssPropWithPragma' + } + ], + output: ` +/** @jsx jsx */ +/** @jsx emotion.jsx */ +import * as emotion from '@emotion/react' +let ele =
+ `.trim() + }, + { + code: ` import {jsx} from '@emotion/react' let ele =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -254,12 +271,10 @@ let ele2 =
`.trim(), errors: [ { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' }, { - message: - 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + messageId: 'cssPropWithPragma' } ], output: ` @@ -278,8 +293,7 @@ let ele2 =
`.trim(), errors: [ { - message: - 'Template literals should be replaced with tagged template literals using `css` when using the css prop' + messageId: 'templateLiterals' } ], output: ` @@ -297,8 +311,7 @@ let ele2 =
`.trim(), errors: [ { - message: - 'Template literals should be replaced with tagged template literals using `css` when using the css prop' + messageId: 'templateLiterals' } ], output: ` @@ -316,8 +329,7 @@ let ele2 =
`.trim(), errors: [ { - message: - 'Template literals should be replaced with tagged template literals using `css` when using the css prop' + messageId: 'templateLiterals' } ], output: ` diff --git a/packages/eslint-plugin/test/rules/no-vanilla.test.js b/packages/eslint-plugin/test/rules/no-vanilla.test.ts similarity index 59% rename from packages/eslint-plugin/test/rules/no-vanilla.test.js rename to packages/eslint-plugin/test/rules/no-vanilla.test.ts index cb7715643..a24e39df5 100644 --- a/packages/eslint-plugin/test/rules/no-vanilla.test.js +++ b/packages/eslint-plugin/test/rules/no-vanilla.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ -import { RuleTester } from 'eslint' -import { rules as emotionRules } from '@emotion/eslint-plugin' +import { TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/no-vanilla' +import { espreeParser } from '../test-utils' -const rule = emotionRules['no-vanilla'] - -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -17,8 +17,6 @@ RuleTester.setDefaultConfig({ } }) -const ruleTester = new RuleTester() - ruleTester.run('no-vanilla', rule, { valid: [{ code: `import { css } from '@emotion/react'` }], invalid: [ @@ -26,7 +24,7 @@ ruleTester.run('no-vanilla', rule, { code: `import { css } from '@emotion/css'`, errors: [ { - message: `Vanilla emotion should not be used` + messageId: 'vanillaEmotion' } ] } diff --git a/packages/eslint-plugin/test/rules/pkg-renaming.test.js b/packages/eslint-plugin/test/rules/pkg-renaming.test.ts similarity index 53% rename from packages/eslint-plugin/test/rules/pkg-renaming.test.js rename to packages/eslint-plugin/test/rules/pkg-renaming.test.ts index 0377b13ee..e15259b3f 100644 --- a/packages/eslint-plugin/test/rules/pkg-renaming.test.js +++ b/packages/eslint-plugin/test/rules/pkg-renaming.test.ts @@ -2,10 +2,12 @@ * @jest-environment node */ -const { RuleTester } = require('eslint') -const rule = require('@emotion/eslint-plugin').rules['pkg-renaming'] +import { TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/pkg-renaming' +import { espreeParser } from '../test-utils' -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -15,8 +17,6 @@ RuleTester.setDefaultConfig({ } }) -const ruleTester = new RuleTester() - ruleTester.run('pkg-renaming', rule, { valid: [ { @@ -31,8 +31,11 @@ ruleTester.run('pkg-renaming', rule, { code: `import { css } from 'emotion'`, errors: [ { - message: - '"emotion" has been renamed to "@emotion/css", please import it from "@emotion/css" instead' + messageId: 'renamePackage', + data: { + beforeName: '"emotion"', + afterName: '"@emotion/css"' + } } ], output: `import { css } from '@emotion/css'` @@ -41,8 +44,11 @@ ruleTester.run('pkg-renaming', rule, { code: `import { css } from '@emotion/core'`, errors: [ { - message: - '"@emotion/core" has been renamed to "@emotion/react", please import it from "@emotion/react" instead' + messageId: 'renamePackage', + data: { + beforeName: '"@emotion/core"', + afterName: '"@emotion/react"' + } } ], output: `import { css } from '@emotion/react'` @@ -51,8 +57,11 @@ ruleTester.run('pkg-renaming', rule, { code: `import css from '@emotion/css'`, errors: [ { - message: - 'The default export of "@emotion/css" in Emotion 10 has been moved to a named export, `css`, from "@emotion/react" in Emotion 11, please import it from "@emotion/react"' + messageId: 'exportChange', + data: { + name: '@emotion/css', + replacement: '@emotion/react' + } } ], output: `import { css } from '@emotion/react'` @@ -61,8 +70,11 @@ ruleTester.run('pkg-renaming', rule, { code: `import css from '@emotion/css/macro'`, errors: [ { - message: - 'The default export of "@emotion/css/macro" in Emotion 10 has been moved to a named export, `css`, from "@emotion/react/macro" in Emotion 11, please import it from "@emotion/react/macro"' + messageId: 'exportChange', + data: { + name: '@emotion/css/macro', + replacement: '@emotion/react/macro' + } } ], output: `import { css } from '@emotion/react/macro'` @@ -71,8 +83,7 @@ ruleTester.run('pkg-renaming', rule, { code: `import {ThemeProvider, withTheme} from 'emotion-theming'`, errors: [ { - message: - '"emotion-theming" has been moved into "@emotion/react", please import its exports from "@emotion/react"' + messageId: 'emotionTheming' } ], output: `import {ThemeProvider, withTheme} from '@emotion/react'` diff --git a/packages/eslint-plugin/test/rules/styled-import.test.js b/packages/eslint-plugin/test/rules/styled-import.test.ts similarity index 64% rename from packages/eslint-plugin/test/rules/styled-import.test.js rename to packages/eslint-plugin/test/rules/styled-import.test.ts index 4909d2dc7..3ca496cbd 100644 --- a/packages/eslint-plugin/test/rules/styled-import.test.js +++ b/packages/eslint-plugin/test/rules/styled-import.test.ts @@ -2,12 +2,12 @@ * @jest-environment node */ -import { RuleTester } from 'eslint' -import { rules as emotionRules } from '@emotion/eslint-plugin' +import { TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/styled-import' +import { espreeParser } from '../test-utils' -const rule = emotionRules['styled-import'] - -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -17,8 +17,6 @@ RuleTester.setDefaultConfig({ } }) -const ruleTester = new RuleTester() - ruleTester.run('emotion styled', rule, { valid: [ { @@ -35,7 +33,7 @@ import styled from 'react-emotion' `.trim(), errors: [ { - message: `styled should be imported from @emotion/styled` + messageId: 'incorrectImport' } ], output: ` diff --git a/packages/eslint-plugin/test/rules/syntax-preference.test.js b/packages/eslint-plugin/test/rules/syntax-preference.test.ts similarity index 68% rename from packages/eslint-plugin/test/rules/syntax-preference.test.js rename to packages/eslint-plugin/test/rules/syntax-preference.test.ts index 58e2c9d0d..97a3b34b3 100644 --- a/packages/eslint-plugin/test/rules/syntax-preference.test.js +++ b/packages/eslint-plugin/test/rules/syntax-preference.test.ts @@ -8,12 +8,12 @@ // Requirements // ------------------------------------------------------------------------------ -import { RuleTester } from 'eslint' -import { rules as emotionRules } from '@emotion/eslint-plugin' +import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/experimental-utils' +import rule from '../../src/rules/syntax-preference' +import { espreeParser } from '../test-utils' -const rule = emotionRules['syntax-preference'] - -RuleTester.setDefaultConfig({ +const ruleTester = new TSESLint.RuleTester({ + parser: espreeParser, parserOptions: { ecmaVersion: 2018, sourceType: 'module', @@ -27,8 +27,6 @@ RuleTester.setDefaultConfig({ // Tests // ------------------------------------------------------------------------------ -const ruleTester = new RuleTester() - ruleTester.run('syntax-preference (string)', rule, { valid: [ // give me some code that won't trigger a warning @@ -68,8 +66,8 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Styles should be written using strings.', - type: 'ObjectExpression' + messageId: 'preferStringStyle', + type: AST_NODE_TYPES.ObjectExpression } ] }, @@ -78,8 +76,8 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Styles should be written using strings.', - type: 'ObjectExpression' + messageId: 'preferStringStyle', + type: AST_NODE_TYPES.ObjectExpression } ] }, @@ -88,8 +86,8 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Styles should be written using strings.', - type: 'ObjectExpression' + messageId: 'preferStringStyle', + type: AST_NODE_TYPES.ObjectExpression } ] }, @@ -98,8 +96,8 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Prefer wrapping your string styles with `css` call.', - type: 'Literal' + messageId: 'preferWrappingWithCSS', + type: AST_NODE_TYPES.Literal } ] }, @@ -108,8 +106,8 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Prefer wrapping your string styles with `css` call.', - type: 'Literal' + messageId: 'preferWrappingWithCSS', + type: AST_NODE_TYPES.Literal } ] }, @@ -118,12 +116,12 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Prefer wrapping your string styles with `css` call.', - type: 'Literal' + messageId: 'preferWrappingWithCSS', + type: AST_NODE_TYPES.Literal }, { - message: 'Styles should be written using strings.', - type: 'ObjectExpression' + messageId: 'preferStringStyle', + type: AST_NODE_TYPES.ObjectExpression } ] }, @@ -132,8 +130,18 @@ ruleTester.run('syntax-preference (string)', rule, { options: ['string'], errors: [ { - message: 'Styles should be written using strings.', - type: 'ObjectExpression' + messageId: 'preferStringStyle', + type: AST_NODE_TYPES.ObjectExpression + } + ] + }, + { + code: `const Foo = () =>
`, + options: ['string'], + errors: [ + { + messageId: 'emptyCssProp', + type: AST_NODE_TYPES.JSXAttribute } ] } @@ -171,8 +179,8 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'TaggedTemplateExpression' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.TaggedTemplateExpression } ] }, @@ -181,8 +189,8 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'TaggedTemplateExpression' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.TaggedTemplateExpression } ] }, @@ -191,8 +199,8 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'TaggedTemplateExpression' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.TaggedTemplateExpression } ] }, @@ -201,8 +209,8 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'Literal' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.Literal } ] }, @@ -211,8 +219,8 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'Literal' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.Literal } ] }, @@ -221,12 +229,12 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'Literal' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.Literal }, { - message: 'Styles should be written using objects.', - type: 'TaggedTemplateExpression' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.TaggedTemplateExpression } ] }, @@ -235,12 +243,22 @@ ruleTester.run('syntax-preference (object)', rule, { options: ['object'], errors: [ { - message: 'Styles should be written using objects.', - type: 'Literal' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.Literal }, { - message: 'Styles should be written using objects.', - type: 'TaggedTemplateExpression' + messageId: 'preferObjectStyle', + type: AST_NODE_TYPES.TaggedTemplateExpression + } + ] + }, + { + code: `const Foo = () =>
`, + options: ['object'], + errors: [ + { + messageId: 'emptyCssProp', + type: AST_NODE_TYPES.JSXAttribute } ] } diff --git a/packages/eslint-plugin/test/test-utils.ts b/packages/eslint-plugin/test/test-utils.ts new file mode 100644 index 000000000..551a54e9d --- /dev/null +++ b/packages/eslint-plugin/test/test-utils.ts @@ -0,0 +1,6 @@ +import resolveFrom from 'resolve-from' + +export const espreeParser: string = resolveFrom( + require.resolve('eslint'), + 'espree' +) diff --git a/tsconfig.json b/tsconfig.json index c5febe599..f084e9804 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "module": "commonjs", "noEmit": true, "skipDefaultLibCheck": true, + "resolveJsonModule": true, "strict": true, "target": "es5" }, diff --git a/yarn.lock b/yarn.lock index 7326b18ce..fa44c47ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5871,6 +5871,14 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== +"@types/eslint@^7.0.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" + integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + "@types/eslint@^7.2.6": version "7.28.2" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.2.tgz#0ff2947cdd305897c52d5372294e8c76f351db68" @@ -6391,7 +6399,7 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@4.33.0", "@typescript-eslint/experimental-utils@^4.0.1": +"@typescript-eslint/experimental-utils@4.33.0", "@typescript-eslint/experimental-utils@^4.0.1", "@typescript-eslint/experimental-utils@^4.30.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==