@compiled/eslint-plugin
Advanced tools
Comparing version 0.11.0 to 0.12.0
@@ -13,4 +13,27 @@ "use strict"; | ||
this.context = context; | ||
this.jsxElement = (0, ast_1.traverseUpToJSXOpeningElement)(this.baseNode); | ||
this.references = context.getScope().references; | ||
this.ignoreIfImported = []; | ||
this.excludeReactComponents = false; | ||
this.parseOptions(this.context.options); | ||
} | ||
parseOptions(options) { | ||
if (options.length === 0) | ||
return; | ||
if (options[0].ignoreIfImported && Array.isArray(options[0].ignoreIfImported)) { | ||
this.ignoreIfImported = options[0].ignoreIfImported; | ||
} | ||
if (typeof options[0].excludeReactComponents === 'boolean') { | ||
this.excludeReactComponents = options[0].excludeReactComponents; | ||
} | ||
else if (options[0].excludeReactComponents !== undefined) { | ||
throw new Error(`Expected the excludeReactComponents option to be a boolean, actually got ${typeof options[0] | ||
.excludeReactComponents}`); | ||
} | ||
} | ||
importsIgnoredLibraries() { | ||
if (!this.ignoreIfImported.length) | ||
return; | ||
return (0, ast_1.findTSLibraryImportDeclarations)(this.context, this.ignoreIfImported).length > 0; | ||
} | ||
handleIdentifier(node) { | ||
@@ -28,8 +51,5 @@ var _a, _b, _c; | ||
const isFunctionParameter = (_c = reference === null || reference === void 0 ? void 0 : reference.resolved) === null || _c === void 0 ? void 0 : _c.defs.find((def) => def.type === 'Parameter'); | ||
const jsxElement = (0, ast_1.traverseUpToJSXOpeningElement)(this.baseNode); | ||
// css property on DOM elements are always fine, e.g. | ||
// <div css={...}> instead of <MyComponent css={...}> | ||
if (jsxElement && | ||
jsxElement.name.type === 'JSXIdentifier' && | ||
(0, ast_1.isDOMElement)(jsxElement.name.name)) { | ||
if ((0, ast_1.isNodeDOMElement)(this.jsxElement)) { | ||
return; | ||
@@ -70,3 +90,3 @@ } | ||
function* fix(fixer) { | ||
const compiledImports = (0, ast_1.findTSCompiledImportDeclarations)(context); | ||
const compiledImports = (0, ast_1.findTSLibraryImportDeclarations)(context); | ||
const source = context.getSourceCode(); | ||
@@ -141,3 +161,8 @@ // The string that `css` from `@compiled/css` is imported as | ||
run() { | ||
this.findStyleNodes(this.baseNode.expression); | ||
if (!this.importsIgnoredLibraries()) { | ||
if (this.excludeReactComponents && !(0, ast_1.isNodeDOMElement)(this.jsxElement)) { | ||
return; | ||
} | ||
this.findStyleNodes(this.baseNode.expression); | ||
} | ||
} | ||
@@ -167,3 +192,21 @@ } | ||
fixable: 'code', | ||
schema: [], | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreIfImported: { | ||
type: 'array', | ||
items: [ | ||
{ | ||
type: 'string', | ||
}, | ||
], | ||
}, | ||
excludeReactComponents: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
@@ -170,0 +213,0 @@ create: createNoCssPropWithoutCssFunctionRule(), |
@@ -17,5 +17,7 @@ import type { TSESTree, TSESLint } from '@typescript-eslint/utils'; | ||
/** | ||
* Re-implementation of findCompiledImportDeclarations for typescript-eslint. | ||
* Re-implementation of findLibraryImportDeclarations for typescript-eslint. | ||
* | ||
* Given a rule, return any `@compiled/react` import declarations in the source code. | ||
* Given a rule, return all imports from the libraries defined in `source` | ||
* in the file. If `source` is not specified, return all import statements | ||
* from `@compiled/react`. | ||
* | ||
@@ -25,3 +27,3 @@ * @param context Rule context | ||
*/ | ||
export declare const findTSCompiledImportDeclarations: (context: TSESLint.RuleContext<string, readonly unknown[]>) => TSESTree.ImportDeclaration[]; | ||
export declare const findTSLibraryImportDeclarations: (context: TSESLint.RuleContext<string, readonly unknown[]>, sources?: string[]) => TSESTree.ImportDeclaration[]; | ||
/** | ||
@@ -31,6 +33,6 @@ * Returns whether the element is a DOM element, which is all lowercase... | ||
* | ||
* @param elementName | ||
* @param jsxElement the JSX element to check | ||
* @returns whether the element is a DOM element (true) or a React component (false) | ||
*/ | ||
export declare const isDOMElement: (elementName: string) => boolean; | ||
export declare const isNodeDOMElement: (jsxElement: TSESTree.JSXOpeningElement) => boolean; | ||
/** | ||
@@ -37,0 +39,0 @@ * Traverses up the AST until it reaches a JSXOpeningElement. Used in conjunction with |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.usesCompiledAPI = exports.findDeclarationWithImport = exports.traverseUpToJSXOpeningElement = exports.isDOMElement = exports.findTSCompiledImportDeclarations = exports.findLibraryImportDeclarations = void 0; | ||
exports.usesCompiledAPI = exports.findDeclarationWithImport = exports.traverseUpToJSXOpeningElement = exports.isNodeDOMElement = exports.findTSLibraryImportDeclarations = exports.findLibraryImportDeclarations = void 0; | ||
const constants_1 = require("./constants"); | ||
// WARNING | ||
// context.getSourceCode() is deprecated, but we still use it here because | ||
// the newer alternative, context.sourceCode, is not supported below | ||
// ESLint 8.40. | ||
// | ||
// We can replace this with context.sourceCode once we are certain that | ||
// all Compiled users are using ESLint 8.40+. | ||
/** | ||
@@ -25,5 +32,7 @@ * Given a rule, return all imports from the libraries defined in `source` | ||
/** | ||
* Re-implementation of findCompiledImportDeclarations for typescript-eslint. | ||
* Re-implementation of findLibraryImportDeclarations for typescript-eslint. | ||
* | ||
* Given a rule, return any `@compiled/react` import declarations in the source code. | ||
* Given a rule, return all imports from the libraries defined in `source` | ||
* in the file. If `source` is not specified, return all import statements | ||
* from `@compiled/react`. | ||
* | ||
@@ -33,8 +42,12 @@ * @param context Rule context | ||
*/ | ||
const findTSCompiledImportDeclarations = (context) => { | ||
const findTSLibraryImportDeclarations = (context, sources = [constants_1.COMPILED_IMPORT]) => { | ||
return context | ||
.getSourceCode() | ||
.ast.body.filter((node) => node.type === 'ImportDeclaration' && node.source.value === constants_1.COMPILED_IMPORT); | ||
.ast.body.filter((node) => node.type === 'ImportDeclaration' && | ||
typeof node.source.value === 'string' && | ||
sources.includes(node.source.value)); | ||
}; | ||
exports.findTSCompiledImportDeclarations = findTSCompiledImportDeclarations; | ||
exports.findTSLibraryImportDeclarations = findTSLibraryImportDeclarations; | ||
const isDOMElementName = (elementName) => elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && | ||
elementName.charAt(0) === elementName.charAt(0).toLowerCase(); | ||
/** | ||
@@ -44,8 +57,9 @@ * Returns whether the element is a DOM element, which is all lowercase... | ||
* | ||
* @param elementName | ||
* @param jsxElement the JSX element to check | ||
* @returns whether the element is a DOM element (true) or a React component (false) | ||
*/ | ||
const isDOMElement = (elementName) => elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && | ||
elementName.charAt(0) === elementName.charAt(0).toLowerCase(); | ||
exports.isDOMElement = isDOMElement; | ||
const isNodeDOMElement = (jsxElement) => { | ||
return jsxElement.name.type === 'JSXIdentifier' && isDOMElementName(jsxElement.name.name); | ||
}; | ||
exports.isNodeDOMElement = isNodeDOMElement; | ||
/** | ||
@@ -52,0 +66,0 @@ * Traverses up the AST until it reaches a JSXOpeningElement. Used in conjunction with |
{ | ||
"name": "@compiled/eslint-plugin", | ||
"version": "0.11.0", | ||
"version": "0.12.0", | ||
"description": "A familiar and performant compile time CSS-in-JS library for React.", | ||
@@ -5,0 +5,0 @@ "homepage": "https://compiledcssinjs.com/docs/pkg-eslint-plugin", |
@@ -109,3 +109,3 @@ # `jsx-pragma` | ||
### `runtime: 'classic' | 'automatic` | ||
### `runtime: 'classic' | 'automatic'` | ||
@@ -112,0 +112,0 @@ What [JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html) to adhere to, |
@@ -133,2 +133,35 @@ import { outdent } from 'outdent'; | ||
`, | ||
{ | ||
code: outdent` | ||
/** @jsx jsx */ | ||
import { jsx } from '@emotion/react'; | ||
<div css={{ color: 'blue' }} /> | ||
`, | ||
options: [{ ignoreIfImported: ['@emotion/react'] }], | ||
}, | ||
{ | ||
code: outdent` | ||
/** @jsx jsx */ | ||
import { jsx } from '@emotion/core'; | ||
<div css={{ color: 'blue' }} /> | ||
`, | ||
options: [{ ignoreIfImported: ['styled-components', '@emotion/core'] }], | ||
}, | ||
{ | ||
code: outdent` | ||
import { css } from '@compiled/react'; | ||
import { jsx } from '@emotion/react'; | ||
<div css={{ color: 'blue' }} /> | ||
`, | ||
options: [{ ignoreIfImported: ['@emotion/react'] }], | ||
}, | ||
{ | ||
code: outdent` | ||
<CustomComponent css={{ color: 'blue' }} /> | ||
`, | ||
options: [{ excludeReactComponents: true }], | ||
}, | ||
], | ||
@@ -455,4 +488,22 @@ | ||
}, | ||
{ | ||
errors: [ | ||
{ | ||
messageId: 'noCssFunction', | ||
}, | ||
], | ||
code: outdent` | ||
import { css } from '@compiled/react'; | ||
<div css={{ color: 'blue' }} /> | ||
`, | ||
output: outdent` | ||
import { css } from '@compiled/react'; | ||
<div css={css({ color: 'blue' })} /> | ||
`, | ||
options: [{ ignoreIfImported: ['@emotion/react'] }], | ||
}, | ||
], | ||
} | ||
); |
import type { TSESTree, TSESLint } from '@typescript-eslint/utils'; | ||
import { | ||
findTSCompiledImportDeclarations, | ||
isDOMElement, | ||
findTSLibraryImportDeclarations, | ||
isNodeDOMElement, | ||
traverseUpToJSXOpeningElement, | ||
@@ -24,3 +24,3 @@ } from '../../utils/ast'; | ||
type Reference = TSESLint.Scope.Reference; | ||
type Context = TSESLint.RuleContext<string, readonly []>; | ||
type Context = TSESLint.RuleContext<string, readonly unknown[]>; | ||
@@ -35,8 +35,39 @@ const findNodeReference = ( | ||
class NoCssPropWithoutCssFunctionRunner { | ||
private excludeReactComponents: boolean; | ||
private ignoreIfImported: string[]; | ||
private jsxElement: TSESTree.JSXOpeningElement; | ||
private references: Reference[]; | ||
constructor(private baseNode: TSESTree.JSXExpressionContainer, private context: Context) { | ||
this.jsxElement = traverseUpToJSXOpeningElement(this.baseNode); | ||
this.references = context.getScope().references; | ||
this.ignoreIfImported = []; | ||
this.excludeReactComponents = false; | ||
this.parseOptions(this.context.options as any); | ||
} | ||
private parseOptions(options: any[]) { | ||
if (options.length === 0) return; | ||
if (options[0].ignoreIfImported && Array.isArray(options[0].ignoreIfImported)) { | ||
this.ignoreIfImported = options[0].ignoreIfImported; | ||
} | ||
if (typeof options[0].excludeReactComponents === 'boolean') { | ||
this.excludeReactComponents = options[0].excludeReactComponents; | ||
} else if (options[0].excludeReactComponents !== undefined) { | ||
throw new Error( | ||
`Expected the excludeReactComponents option to be a boolean, actually got ${typeof options[0] | ||
.excludeReactComponents}` | ||
); | ||
} | ||
} | ||
private importsIgnoredLibraries() { | ||
if (!this.ignoreIfImported.length) return; | ||
return findTSLibraryImportDeclarations(this.context, this.ignoreIfImported).length > 0; | ||
} | ||
private handleIdentifier(node: TSESTree.Identifier) { | ||
@@ -56,11 +87,5 @@ // Resolve the variable for the reference | ||
const jsxElement = traverseUpToJSXOpeningElement(this.baseNode); | ||
// css property on DOM elements are always fine, e.g. | ||
// <div css={...}> instead of <MyComponent css={...}> | ||
if ( | ||
jsxElement && | ||
jsxElement.name.type === 'JSXIdentifier' && | ||
isDOMElement(jsxElement.name.name) | ||
) { | ||
if (isNodeDOMElement(this.jsxElement)) { | ||
return; | ||
@@ -104,3 +129,3 @@ } | ||
function* fix(fixer: TSESLint.RuleFixer) { | ||
const compiledImports = findTSCompiledImportDeclarations(context); | ||
const compiledImports = findTSLibraryImportDeclarations(context); | ||
const source = context.getSourceCode(); | ||
@@ -177,3 +202,8 @@ | ||
run() { | ||
this.findStyleNodes(this.baseNode.expression); | ||
if (!this.importsIgnoredLibraries()) { | ||
if (this.excludeReactComponents && !isNodeDOMElement(this.jsxElement)) { | ||
return; | ||
} | ||
this.findStyleNodes(this.baseNode.expression); | ||
} | ||
} | ||
@@ -212,5 +242,23 @@ } | ||
fixable: 'code', | ||
schema: [], | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreIfImported: { | ||
type: 'array', | ||
items: [ | ||
{ | ||
type: 'string', | ||
}, | ||
], | ||
}, | ||
excludeReactComponents: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: createNoCssPropWithoutCssFunctionRule(), | ||
}; |
@@ -85,1 +85,26 @@ # `no-css-prop-without-css-function` | ||
``` | ||
## Options | ||
This rule supports the following options: | ||
### `ignoreIfImported: string[]` | ||
An array of libraries, each specified as a string (e.g. `['@emotion/core', '@emotion/react']`). If any of these libraries is detected as being imported in the current file, then this ESLint rule does not run. | ||
This is useful if you do not want `no-css-prop-without-css-function` to conflict with files where Emotion's JSX pragma is being used (such as the below example). | ||
```tsx | ||
/** @jsx jsx */ | ||
import { jsx } from '@emotion/react'; | ||
// ... | ||
``` | ||
This is an empty array by default. | ||
### `excludeReactComponents: boolean` | ||
Whether to exclude `css` attributes of React components from being affected by this ESLint rule. We assume that an element is a React component if it starts with a capital letter. | ||
This is false by default. |
@@ -7,2 +7,10 @@ import type { TSESTree, TSESLint } from '@typescript-eslint/utils'; | ||
// WARNING | ||
// context.getSourceCode() is deprecated, but we still use it here because | ||
// the newer alternative, context.sourceCode, is not supported below | ||
// ESLint 8.40. | ||
// | ||
// We can replace this with context.sourceCode once we are certain that | ||
// all Compiled users are using ESLint 8.40+. | ||
/** | ||
@@ -34,5 +42,7 @@ * Given a rule, return all imports from the libraries defined in `source` | ||
/** | ||
* Re-implementation of findCompiledImportDeclarations for typescript-eslint. | ||
* Re-implementation of findLibraryImportDeclarations for typescript-eslint. | ||
* | ||
* Given a rule, return any `@compiled/react` import declarations in the source code. | ||
* Given a rule, return all imports from the libraries defined in `source` | ||
* in the file. If `source` is not specified, return all import statements | ||
* from `@compiled/react`. | ||
* | ||
@@ -42,4 +52,5 @@ * @param context Rule context | ||
*/ | ||
export const findTSCompiledImportDeclarations = ( | ||
context: TSESLint.RuleContext<string, readonly unknown[]> | ||
export const findTSLibraryImportDeclarations = ( | ||
context: TSESLint.RuleContext<string, readonly unknown[]>, | ||
sources = [COMPILED_IMPORT] | ||
): TSESTree.ImportDeclaration[] => { | ||
@@ -50,6 +61,12 @@ return context | ||
(node): node is TSESTree.ImportDeclaration => | ||
node.type === 'ImportDeclaration' && node.source.value === COMPILED_IMPORT | ||
node.type === 'ImportDeclaration' && | ||
typeof node.source.value === 'string' && | ||
sources.includes(node.source.value) | ||
); | ||
}; | ||
const isDOMElementName = (elementName: string): boolean => | ||
elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && | ||
elementName.charAt(0) === elementName.charAt(0).toLowerCase(); | ||
/** | ||
@@ -59,8 +76,8 @@ * Returns whether the element is a DOM element, which is all lowercase... | ||
* | ||
* @param elementName | ||
* @param jsxElement the JSX element to check | ||
* @returns whether the element is a DOM element (true) or a React component (false) | ||
*/ | ||
export const isDOMElement = (elementName: string): boolean => | ||
elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && | ||
elementName.charAt(0) === elementName.charAt(0).toLowerCase(); | ||
export const isNodeDOMElement = (jsxElement: TSESTree.JSXOpeningElement): boolean => { | ||
return jsxElement.name.type === 'JSXIdentifier' && isDOMElementName(jsxElement.name.name); | ||
}; | ||
@@ -67,0 +84,0 @@ /** |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
389165
9401