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

feat(eslint-plugin): migrate to typescript #2568

Merged
merged 18 commits into from
Apr 11, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-zebras-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@emotion/eslint-plugin': patch
---

An empty css prop (`<div css />`) will now raise an error in the `@emotion/syntax-preference` rule instead of crashing on this case.
5 changes: 5 additions & 0 deletions .changeset/modern-penguins-play.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/sharp-trees-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@emotion/eslint-plugin': patch
---

Fixed a crash on empty css prop (`<div css />`) in the `@emotion/jsx-import` rule.
7 changes: 6 additions & 1 deletion packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
42 changes: 0 additions & 42 deletions packages/eslint-plugin/src/rules/import-from-emotion.js

This file was deleted.

68 changes: 68 additions & 0 deletions packages/eslint-plugin/src/rules/import-from-emotion.ts
Original file line number Diff line number Diff line change
@@ -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'")
}
})
}
}
}
}
})
Original file line number Diff line number Diff line change
@@ -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]+)/

Expand All @@ -6,9 +9,35 @@ const JSX_IMPORT_SOURCE_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/
// to
// <div css={css`color:hotpink;`} /> + 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<RuleOptions, keyof typeof messages>({
name: __filename,
meta: {
docs: {
category: 'Possible Errors',
description: 'Ensure jsx from @emotion/react is imported',
recommended: false
},
fixable: 'code',
messages,
schema: {
type: 'array',
items: {
Expand All @@ -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) {
Expand All @@ -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
}
})
Expand All @@ -65,21 +96,30 @@ 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],
`/** @jsxImportSource ${importSource} */\n`
)
}
})
} 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} */`
)
}
Expand All @@ -95,26 +135,28 @@ 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')
) {
emotionCoreNode = x

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
Expand All @@ -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`
Expand All @@ -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 }')
}

Expand All @@ -174,43 +224,53 @@ 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')
]
}
})
}
}
}
}
}
})
Loading