Skip to content

Commit

Permalink
feat: add new rule no-jsx-html-comments, close #13
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Aug 2, 2019
1 parent 114831c commit 67ba91e
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 97 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ before_install:
script:
- set -e
- yarn lint
- yarn test
- yarn build

before_deploy:
Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)

> [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.
> [ESLint] Parser/Plugin for [MDX], helps you lint all ES syntaxes excluding `code` block of course.
> Work perfectly with `eslint-plugin-import`, `eslint-plugin-prettier` or any other eslint plugins.
## Install
Expand Down Expand Up @@ -57,6 +57,7 @@ npm i -D @rxts/eslint-plugin-mdx
"parser": "@rxts/eslint-plugin-mdx",
"plugins": ["@rxts/mdx"],
"rules": {
"@rxts/mdx/no-jsx-html-comments": 2,
"@rxts/mdx/no-unused-expressions": 2,
"no-unused-expressions": 0,
"react/react-in-jsx-scope": 0
Expand All @@ -72,7 +73,7 @@ npm i -D @rxts/eslint-plugin-mdx
eslint . --ext js,mdx
```

3. Custom parser for ES syntax is also supported:
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:

```json
{
Expand All @@ -88,14 +89,38 @@ npm i -D @rxts/eslint-plugin-mdx
}
```

## FAQ

### Why I need to use `overrides`?

This parser/plugin should only affects `.mdx` files, of course you manually config it on your own risk.

## Rules

### @rxts/mdx/no-jsx-html-comments

HTML style comments in jsx block is invalid, this rule will help you to fix it by transforming it to JSX style comments.

### @rxts/mdx/no-unused-expressions

`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.

## Limitation

> 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.
> 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.

I have a very preliminary idea to integrate with [remake-lint].

## Changelog

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

## License

[MIT](http://opensource.org/licenses/MIT)
[MIT]

[eslint]: https://eslint.org
[mdx]: https://github.com/mdx-js/mdx
[mit]: http://opensource.org/licenses/MIT
[markdownlint]: https://github.com/markdownlint/markdownlint
[remake-lint]: https://github.com/remarkjs/remark-lint
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import path from 'path'
export * from './helper'
export * from './normalizer'
export * from './parser'
export * from './processors'
export * from './regexp'
export * from './rules'
export * from './traverse'
Expand All @@ -13,6 +12,7 @@ export const configs = {
parser: path.resolve(__dirname, 'parser'),
plugins: ['@rxts/mdx'],
rules: {
'@rxts/mdx/no-jsx-html-comments': 2,
'@rxts/mdx/no-unused-expressions': 2,
'no-unused-expressions': 0,
'react/react-in-jsx-scope': 0,
Expand Down
55 changes: 18 additions & 37 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,27 @@
import { isComment, COMMENT_CONTENT_REGEX } from './regexp'
import { mdxProcessor } from './parser'

import { Node, Parent } from 'unist'
import { Node } from 'unist'

export const normalizeJsxNode = (node: Node) => {
const rawText = node.value as string
if (!isComment(rawText)) {
const matched = rawText.match(COMMENT_CONTENT_REGEX)
if (matched) {
node.value = rawText.replace(
COMMENT_CONTENT_REGEX,
(_matched, $0) => `{/*${$0}*/}`,
)
}
let rawText = node.value as string

if (isComment(rawText)) {
return node
}
return node
}

export const normalizeMdx = (source: string) => {
const lines = source.split('\n').length
const { children } = mdxProcessor.parse(source) as Parent
let lastLine: number
return children.reduce((result, node, index) => {
const {
position: { start, end },
} = node
const startLine = start.line
const endLine = end.line
if (lastLine != null && lastLine !== startLine) {
result += '\n'.repeat(startLine - lastLine)
}
if (node.type === 'jsx') {
result += normalizeJsxNode(node).value
} else {
result += source.slice(start.offset, end.offset)
}
const matched = rawText.match(COMMENT_CONTENT_REGEX)

if (index === children.length - 1 && endLine < lines) {
result += '\n'.repeat(lines - endLine)
}
if (!matched) {
return node
}

node.jsxType = 'JSXElementWithHTMLComments'
node.raw = rawText
rawText = node.value = rawText.replace(
COMMENT_CONTENT_REGEX,
(_matched, $0, $1, $2) =>
`{/${'*'.repeat($0.length - 1)}${$1}${'*'.repeat($2.length - 1)}/}`,
)

lastLine = endLine
return result
}, '')
return node
}
49 changes: 34 additions & 15 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import unified from 'unified'

import { normalizeJsxNode } from './normalizer'
import { normalizePosition, restoreNodeLocation } from './helper'
import { isComment } from './regexp'
import { traverse } from './traverse'

import { AST, Linter } from 'eslint'
import { Parent } from 'unist'
import { Parent, Node } from 'unist'

export const AST_PROPS = ['body', 'comments', 'tokens'] as const
export const ES_NODE_TYPES = ['export', 'import', 'jsx'] as const
Expand All @@ -28,26 +29,29 @@ export const mdxProcessor = unified()
export const parseForESLint = (
code: string,
options: Linter.ParserOptions = {},
): Linter.ESLintParseResult => {
) => {
let { parser } = options

if (parser) {
if (typeof parser === 'string') {
parser = require(parser).parse
} else {
if (typeof parser === 'object') {
parser = parser.parseForESLint || parser.parse
}
if (typeof parser !== 'function') {
throw new TypeError(
`Invalid custom parser for \`eslint-plugin-mdx\`: ${parser}`,
)
}
parser = require(parser)
}

if (typeof parser === 'object') {
parser = parser.parseForESLint || parser.parse
}

if (typeof parser !== 'function') {
throw new TypeError(
`Invalid custom parser for \`eslint-plugin-mdx\`: ${options.parser}`,
)
}
} else {
try {
// try to load babel-eslint automatically
parser = require(require.resolve('babel-eslint')).parse
// eslint-disable-next-line @typescript-eslint/no-var-requires
const babelEslint = require('babel-eslint')
parser = babelEslint.parseForESLint || babelEslint.parse
} catch (e) {
parser = esParse
}
Expand All @@ -63,13 +67,22 @@ export const parseForESLint = (
comments: [],
tokens: [],
}
const services = {
JSXElementsWithHTMLComments: [] as Node[],
}

traverse(root, {
enter(node) {
if (!ES_NODE_TYPES.includes(node.type as EsNodeType)) {
return
}

normalizeJsxNode(node)

if (node.jsxType === 'JSXElementWithHTMLComments') {
services.JSXElementsWithHTMLComments.push(node)
}

const rawText = node.value as string

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

let program: AST.Program
let program: AST.Program | Linter.ESLintParseResult

try {
program = parser(rawText, options)
Expand All @@ -94,6 +107,10 @@ export const parseForESLint = (
throw e
}

if ('ast' in program) {
program = program.ast
}

const offset = start - program.range[0]

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

return {
ast,
}
parserServices: services,
services,
} as Linter.ESLintParseResult
}
10 changes: 0 additions & 10 deletions src/processors.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/regexp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const openTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*>'
const closeTag = '<\\s*\\/[A-Za-z]*[A-Za-z0-9\\.\\-]*\\s*>'
const selfClosingTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*\\/?>'
const comment = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->'
const commentOpen = '<!---?'
const commentClose = '-->'
const commentOpen = '<(!---*)'
const commentClose = '(-*--)>'

export const OPEN_TAG_REGEX = new RegExp(`^(?:${openTag})$`)
export const CLOSE_TAG_REGEX = new RegExp(`^(?:${closeTag})$`)
Expand Down
4 changes: 4 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { noJsxHtmlComments } from './no-jsx-html-comments'
import { noUnUsedExpressions } from './no-unused-expressions'

export { noJsxHtmlComments, noUnUsedExpressions }

export const rules = {
'no-jsx-html-comments': noJsxHtmlComments,
'no-unused-expressions': noUnUsedExpressions,
}
63 changes: 63 additions & 0 deletions src/rules/no-jsx-html-comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types'

import { Rule } from 'eslint'
import { Node } from 'unist'

export const noJsxHtmlComments: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
description: 'Forbid invalid html style comments in jsx block',
category: 'SyntaxError',
recommended: true,
},
messages: {
jdxHtmlComments: 'html style comments are invalid in jsx: {{ raw }}',
},
fixable: 'code',
schema: [],
},
create(context) {
return {
ExpressionStatement(node: ExpressionStatementWithParent) {
const invalidNodes: Node[] =
context.parserServices.JSXElementsWithHTMLComments

if (
!JSX_TYPES.includes(node.expression.type as JsxType) ||
node.parent.type !== 'Program' ||
!invalidNodes ||
!invalidNodes.length
) {
return
}

const invalidNode = invalidNodes.shift()
// unist column is 1-indexed, but estree is 0-indexed...
const { start, end } = invalidNode.position
context.report({
messageId: 'jdxHtmlComments',
data: {
raw: invalidNode.raw as string,
},
loc: {
start: {
...start,
column: start.column - 1,
},
end: {
...end,
column: end.column - 1,
},
},
fix(fixer) {
return fixer.replaceTextRange(
[start.offset, end.offset],
invalidNode.value as string,
)
},
})
},
}
},
} as Rule.RuleModule
13 changes: 2 additions & 11 deletions src/rules/no-unused-expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,9 @@

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

import { Rule } from 'eslint'
import { ExpressionStatement, Node } from 'estree'

export const JSX_TYPES = ['JSXElement', 'JSXFragment'] as const

export type JsxType = (typeof JSX_TYPES)[number]
import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types'

export interface ExpressionStatementWithParent extends ExpressionStatement {
parent?: {
type: Node['type']
}
}
import { Rule } from 'eslint'

export const noUnUsedExpressions: Rule.RuleModule = {
...esLintNoUnUsedExpressions,
Expand Down
11 changes: 11 additions & 0 deletions src/rules/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ExpressionStatement, Node } from 'estree'

export const JSX_TYPES = ['JSXElement', 'JSXFragment'] as const

export type JsxType = (typeof JSX_TYPES)[number]

export interface ExpressionStatementWithParent extends ExpressionStatement {
parent?: {
type: Node['type']
}
}
Loading

0 comments on commit 67ba91e

Please sign in to comment.