Skip to content

Commit 67ba91e

Browse files
committed
feat: add new rule no-jsx-html-comments, close #13
1 parent 114831c commit 67ba91e

File tree

13 files changed

+177
-97
lines changed

13 files changed

+177
-97
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ before_install:
1313
script:
1414
- set -e
1515
- yarn lint
16+
- yarn test
1617
- yarn build
1718

1819
before_deploy:

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
1818
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
1919

20-
> [ESLint](https://eslint.org/) Parser/Plugin for [MDX](https://github.com/mdx-js/mdx), helps you lint all ES syntaxes excluding `code` block of course.
20+
> [ESLint] Parser/Plugin for [MDX], helps you lint all ES syntaxes excluding `code` block of course.
2121
> Work perfectly with `eslint-plugin-import`, `eslint-plugin-prettier` or any other eslint plugins.
2222
2323
## Install
@@ -57,6 +57,7 @@ npm i -D @rxts/eslint-plugin-mdx
5757
"parser": "@rxts/eslint-plugin-mdx",
5858
"plugins": ["@rxts/mdx"],
5959
"rules": {
60+
"@rxts/mdx/no-jsx-html-comments": 2,
6061
"@rxts/mdx/no-unused-expressions": 2,
6162
"no-unused-expressions": 0,
6263
"react/react-in-jsx-scope": 0
@@ -72,7 +73,7 @@ npm i -D @rxts/eslint-plugin-mdx
7273
eslint . --ext js,mdx
7374
```
7475

75-
3. Custom parser for ES syntax is also supported:
76+
3. Custom parser for ES syntax is also supported, although `babel-eslint` will be detected automatically what means you actually do not need to do this:
7677

7778
```json
7879
{
@@ -88,14 +89,38 @@ npm i -D @rxts/eslint-plugin-mdx
8889
}
8990
```
9091

92+
## FAQ
93+
94+
### Why I need to use `overrides`?
95+
96+
This parser/plugin should only affects `.mdx` files, of course you manually config it on your own risk.
97+
98+
## Rules
99+
100+
### @rxts/mdx/no-jsx-html-comments
101+
102+
HTML style comments in jsx block is invalid, this rule will help you to fix it by transforming it to JSX style comments.
103+
104+
### @rxts/mdx/no-unused-expressions
105+
106+
`MDX` can render `jsx` block automatically without exporting them, but `eslint` will report `no-unused-expressions` issue which could be unexpected, this rule is a replacement of it, so make sure that you've turned off the original `no-unused-expressions` rule.
107+
91108
## Limitation
92109

93-
> This parser/plugin can only handle ES syntaxes for you, markdown related syntaxes will just be ignored, you can use [markdownlint](https://github.com/markdownlint/markdownlint) to lint that part.
110+
> This parser/plugin can only handle ES syntaxes for you, markdown related syntaxes will just be ignored, you can use [markdownlint] or [remake-lint] to lint that part.
111+
112+
I have a very preliminary idea to integrate with [remake-lint].
94113

95114
## Changelog
96115

97116
Detailed changes for each release are documented in [CHANGELOG.md](./CHANGELOG.md).
98117

99118
## License
100119

101-
[MIT](http://opensource.org/licenses/MIT)
120+
[MIT]
121+
122+
[eslint]: https://eslint.org
123+
[mdx]: https://github.com/mdx-js/mdx
124+
[mit]: http://opensource.org/licenses/MIT
125+
[markdownlint]: https://github.com/markdownlint/markdownlint
126+
[remake-lint]: https://github.com/remarkjs/remark-lint

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import path from 'path'
33
export * from './helper'
44
export * from './normalizer'
55
export * from './parser'
6-
export * from './processors'
76
export * from './regexp'
87
export * from './rules'
98
export * from './traverse'
@@ -13,6 +12,7 @@ export const configs = {
1312
parser: path.resolve(__dirname, 'parser'),
1413
plugins: ['@rxts/mdx'],
1514
rules: {
15+
'@rxts/mdx/no-jsx-html-comments': 2,
1616
'@rxts/mdx/no-unused-expressions': 2,
1717
'no-unused-expressions': 0,
1818
'react/react-in-jsx-scope': 0,

src/normalizer.ts

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
11
import { isComment, COMMENT_CONTENT_REGEX } from './regexp'
2-
import { mdxProcessor } from './parser'
32

4-
import { Node, Parent } from 'unist'
3+
import { Node } from 'unist'
54

65
export const normalizeJsxNode = (node: Node) => {
7-
const rawText = node.value as string
8-
if (!isComment(rawText)) {
9-
const matched = rawText.match(COMMENT_CONTENT_REGEX)
10-
if (matched) {
11-
node.value = rawText.replace(
12-
COMMENT_CONTENT_REGEX,
13-
(_matched, $0) => `{/*${$0}*/}`,
14-
)
15-
}
6+
let rawText = node.value as string
7+
8+
if (isComment(rawText)) {
9+
return node
1610
}
17-
return node
18-
}
1911

20-
export const normalizeMdx = (source: string) => {
21-
const lines = source.split('\n').length
22-
const { children } = mdxProcessor.parse(source) as Parent
23-
let lastLine: number
24-
return children.reduce((result, node, index) => {
25-
const {
26-
position: { start, end },
27-
} = node
28-
const startLine = start.line
29-
const endLine = end.line
30-
if (lastLine != null && lastLine !== startLine) {
31-
result += '\n'.repeat(startLine - lastLine)
32-
}
33-
if (node.type === 'jsx') {
34-
result += normalizeJsxNode(node).value
35-
} else {
36-
result += source.slice(start.offset, end.offset)
37-
}
12+
const matched = rawText.match(COMMENT_CONTENT_REGEX)
3813

39-
if (index === children.length - 1 && endLine < lines) {
40-
result += '\n'.repeat(lines - endLine)
41-
}
14+
if (!matched) {
15+
return node
16+
}
17+
18+
node.jsxType = 'JSXElementWithHTMLComments'
19+
node.raw = rawText
20+
rawText = node.value = rawText.replace(
21+
COMMENT_CONTENT_REGEX,
22+
(_matched, $0, $1, $2) =>
23+
`{/${'*'.repeat($0.length - 1)}${$1}${'*'.repeat($2.length - 1)}/}`,
24+
)
4225

43-
lastLine = endLine
44-
return result
45-
}, '')
26+
return node
4627
}

src/parser.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import remarkParse from 'remark-parse'
77
import remarkStringify from 'remark-stringify'
88
import unified from 'unified'
99

10+
import { normalizeJsxNode } from './normalizer'
1011
import { normalizePosition, restoreNodeLocation } from './helper'
1112
import { isComment } from './regexp'
1213
import { traverse } from './traverse'
1314

1415
import { AST, Linter } from 'eslint'
15-
import { Parent } from 'unist'
16+
import { Parent, Node } from 'unist'
1617

1718
export const AST_PROPS = ['body', 'comments', 'tokens'] as const
1819
export const ES_NODE_TYPES = ['export', 'import', 'jsx'] as const
@@ -28,26 +29,29 @@ export const mdxProcessor = unified()
2829
export const parseForESLint = (
2930
code: string,
3031
options: Linter.ParserOptions = {},
31-
): Linter.ESLintParseResult => {
32+
) => {
3233
let { parser } = options
3334

3435
if (parser) {
3536
if (typeof parser === 'string') {
36-
parser = require(parser).parse
37-
} else {
38-
if (typeof parser === 'object') {
39-
parser = parser.parseForESLint || parser.parse
40-
}
41-
if (typeof parser !== 'function') {
42-
throw new TypeError(
43-
`Invalid custom parser for \`eslint-plugin-mdx\`: ${parser}`,
44-
)
45-
}
37+
parser = require(parser)
38+
}
39+
40+
if (typeof parser === 'object') {
41+
parser = parser.parseForESLint || parser.parse
42+
}
43+
44+
if (typeof parser !== 'function') {
45+
throw new TypeError(
46+
`Invalid custom parser for \`eslint-plugin-mdx\`: ${options.parser}`,
47+
)
4648
}
4749
} else {
4850
try {
4951
// try to load babel-eslint automatically
50-
parser = require(require.resolve('babel-eslint')).parse
52+
// eslint-disable-next-line @typescript-eslint/no-var-requires
53+
const babelEslint = require('babel-eslint')
54+
parser = babelEslint.parseForESLint || babelEslint.parse
5155
} catch (e) {
5256
parser = esParse
5357
}
@@ -63,13 +67,22 @@ export const parseForESLint = (
6367
comments: [],
6468
tokens: [],
6569
}
70+
const services = {
71+
JSXElementsWithHTMLComments: [] as Node[],
72+
}
6673

6774
traverse(root, {
6875
enter(node) {
6976
if (!ES_NODE_TYPES.includes(node.type as EsNodeType)) {
7077
return
7178
}
7279

80+
normalizeJsxNode(node)
81+
82+
if (node.jsxType === 'JSXElementWithHTMLComments') {
83+
services.JSXElementsWithHTMLComments.push(node)
84+
}
85+
7386
const rawText = node.value as string
7487

7588
// fix #4
@@ -80,7 +93,7 @@ export const parseForESLint = (
8093
const { loc, start } = normalizePosition(node.position)
8194
const startLine = loc.start.line - 1 //! line is 1-indexed, change to 0-indexed to simplify usage
8295

83-
let program: AST.Program
96+
let program: AST.Program | Linter.ESLintParseResult
8497

8598
try {
8699
program = parser(rawText, options)
@@ -94,6 +107,10 @@ export const parseForESLint = (
94107
throw e
95108
}
96109

110+
if ('ast' in program) {
111+
program = program.ast
112+
}
113+
97114
const offset = start - program.range[0]
98115

99116
AST_PROPS.forEach(prop =>
@@ -110,5 +127,7 @@ export const parseForESLint = (
110127

111128
return {
112129
ast,
113-
}
130+
parserServices: services,
131+
services,
132+
} as Linter.ESLintParseResult
114133
}

src/processors.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/regexp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ const openTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*>'
2323
const closeTag = '<\\s*\\/[A-Za-z]*[A-Za-z0-9\\.\\-]*\\s*>'
2424
const selfClosingTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*\\/?>'
2525
const comment = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->'
26-
const commentOpen = '<!---?'
27-
const commentClose = '-->'
26+
const commentOpen = '<(!---*)'
27+
const commentClose = '(-*--)>'
2828

2929
export const OPEN_TAG_REGEX = new RegExp(`^(?:${openTag})$`)
3030
export const CLOSE_TAG_REGEX = new RegExp(`^(?:${closeTag})$`)

src/rules/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { noJsxHtmlComments } from './no-jsx-html-comments'
12
import { noUnUsedExpressions } from './no-unused-expressions'
23

4+
export { noJsxHtmlComments, noUnUsedExpressions }
5+
36
export const rules = {
7+
'no-jsx-html-comments': noJsxHtmlComments,
48
'no-unused-expressions': noUnUsedExpressions,
59
}

src/rules/no-jsx-html-comments.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types'
2+
3+
import { Rule } from 'eslint'
4+
import { Node } from 'unist'
5+
6+
export const noJsxHtmlComments: Rule.RuleModule = {
7+
meta: {
8+
type: 'problem',
9+
docs: {
10+
description: 'Forbid invalid html style comments in jsx block',
11+
category: 'SyntaxError',
12+
recommended: true,
13+
},
14+
messages: {
15+
jdxHtmlComments: 'html style comments are invalid in jsx: {{ raw }}',
16+
},
17+
fixable: 'code',
18+
schema: [],
19+
},
20+
create(context) {
21+
return {
22+
ExpressionStatement(node: ExpressionStatementWithParent) {
23+
const invalidNodes: Node[] =
24+
context.parserServices.JSXElementsWithHTMLComments
25+
26+
if (
27+
!JSX_TYPES.includes(node.expression.type as JsxType) ||
28+
node.parent.type !== 'Program' ||
29+
!invalidNodes ||
30+
!invalidNodes.length
31+
) {
32+
return
33+
}
34+
35+
const invalidNode = invalidNodes.shift()
36+
// unist column is 1-indexed, but estree is 0-indexed...
37+
const { start, end } = invalidNode.position
38+
context.report({
39+
messageId: 'jdxHtmlComments',
40+
data: {
41+
raw: invalidNode.raw as string,
42+
},
43+
loc: {
44+
start: {
45+
...start,
46+
column: start.column - 1,
47+
},
48+
end: {
49+
...end,
50+
column: end.column - 1,
51+
},
52+
},
53+
fix(fixer) {
54+
return fixer.replaceTextRange(
55+
[start.offset, end.offset],
56+
invalidNode.value as string,
57+
)
58+
},
59+
})
60+
},
61+
}
62+
},
63+
} as Rule.RuleModule

src/rules/no-unused-expressions.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,9 @@
33

44
import esLintNoUnUsedExpressions from 'eslint/lib/rules/no-unused-expressions'
55

6-
import { Rule } from 'eslint'
7-
import { ExpressionStatement, Node } from 'estree'
8-
9-
export const JSX_TYPES = ['JSXElement', 'JSXFragment'] as const
10-
11-
export type JsxType = (typeof JSX_TYPES)[number]
6+
import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types'
127

13-
export interface ExpressionStatementWithParent extends ExpressionStatement {
14-
parent?: {
15-
type: Node['type']
16-
}
17-
}
8+
import { Rule } from 'eslint'
189

1910
export const noUnUsedExpressions: Rule.RuleModule = {
2011
...esLintNoUnUsedExpressions,

0 commit comments

Comments
 (0)