Skip to content

Commit

Permalink
feat: add new rule no-unescaped-entities
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Aug 2, 2019
1 parent fa3682a commit dca8633
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 150 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
files: ['*.d.ts'],
rules: {
'import/order': 0,
'import/no-duplicates': 0,
'import/no-unresolved': 0,
},
},
Expand Down
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ node_js: --lts

cache: yarn

branches:
except:
- develop

before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"prepublishOnly": "yarn build",
"build": "tsc -P src",
"test": "jest",
"lint": "eslint . --ext js,mdx,ts,tsx"
"lint": "EFF_NO_LINK_RULES=true eslint . --ext js,mdx,ts,tsx -f friendly"
},
"keywords": [
"eslint",
Expand Down Expand Up @@ -45,6 +45,7 @@
"commitlint": "^8.1.0",
"eslint": "^6.1.0",
"eslint-config-1stg": "~5.4.1",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-jest": "^22.14.1",
"husky": "^3.0.2",
"jest": "^24.8.0",
Expand Down
70 changes: 5 additions & 65 deletions src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {
isOpenTag,
isCloseTag,
isComment,
isSelfClosingTag,
isOpenCloseTag,
} from './regexp'

import { Node, Position } from 'unist'
import { Position } from 'unist'
import { AST } from 'eslint'
// SourceLocation` is not exported from estree, but it is actually working
// `SourceLocation` is not exported from estree, but it is actually working
// eslint-disable-next-line import/named
import { SourceLocation } from 'estree'

Expand Down Expand Up @@ -82,59 +74,7 @@ export function restoreNodeLocation<T extends BaseNode>(
}
}

// fix #7
export const combineJsxNodes = (nodes: Node[]) => {
let offset = 0
const jsxNodes: Node[] = []
const { length } = nodes
return nodes.reduce<Node[]>((acc, node, index) => {
if (node.type === 'jsx') {
const rawText = node.value as string
if (isOpenTag(rawText as string)) {
offset++
jsxNodes.push(node)
} else {
if (isCloseTag(rawText)) {
offset--
} else if (
!isComment(rawText) &&
!isSelfClosingTag(rawText) &&
!isOpenCloseTag(rawText)
) {
const { start } = node.position
throw Object.assign(
new SyntaxError(
`'Unknown node type: ${JSON.stringify(
node.type,
)}, text: ${JSON.stringify(rawText)}`,
),
{
lineNumber: start.line,
column: start.column,
index: start.offset,
},
)
}

jsxNodes.push(node)
export const first = <T>(items: T[] | ReadonlyArray<T>) => items && items[0]

if (!offset || index === length - 1) {
acc.push({
type: 'jsx',
value: jsxNodes.reduce((acc, { value }) => (acc += value), ''),
position: {
start: jsxNodes[0].position.start,
end: jsxNodes[jsxNodes.length - 1].position.end,
},
})
jsxNodes.length = 0
}
}
} else if (offset) {
jsxNodes.push(node)
} else {
acc.push(node)
}
return acc
}, [])
}
export const last = <T>(items: T[] | ReadonlyArray<T>) =>
items && items[items.length - 1]
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ export const configs = {
plugins: ['@rxts/mdx'],
rules: {
'@rxts/mdx/no-jsx-html-comments': 2,
'@rxts/mdx/no-unescaped-entities': 2,
'@rxts/mdx/no-unused-expressions': 2,
'no-unused-expressions': 0,
'react/no-unescaped-entities': 0,
'react/react-in-jsx-scope': 0,
},
},
Expand Down
75 changes: 62 additions & 13 deletions src/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,76 @@
import { last } from './helper'
import { isComment, COMMENT_CONTENT_REGEX } from './regexp'

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

export const normalizeJsxNode = (node: Node) => {
let rawText = node.value as string
export interface Comment {
fixed: string
loc: {
start: Point
end: Point
}
origin: string
}

export const normalizeJsxNode = (node: Node, parent?: Parent) => {
const value = node.value as string

if (isComment(rawText)) {
if (isComment(value) || !parent) {
return node
}

const matched = rawText.match(COMMENT_CONTENT_REGEX)
const matched = value.match(COMMENT_CONTENT_REGEX)

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)}/}`,
)
const comments: Comment[] = []
const {
position: {
start: { line, column, offset: startOffset },
},
} = node

return node
return Object.assign(node, {
data: {
...node.data,
jsxType: 'JSXElementWithHTMLComments',
comments,
// jsx in paragraph is considered as plain html in mdx, what means html style comments are valid
// TODO: in this case, jsx style comments could be a mistake
inline: parent.type !== 'root',
},
value: value.replace(
COMMENT_CONTENT_REGEX,
(matched: string, $0: string, $1: string, $2: string, offset: number) => {
const endOffset = offset + matched.length
const startLines = value.slice(0, offset).split('\n')
const endLines = value.slice(0, endOffset).split('\n')
const fixed = `{/${'*'.repeat($0.length - 2)}${$1}${'*'.repeat(
$2.length - 2,
)}/}`
const startLineOffset = startLines.length - 1
const endLineOffset = endLines.length - 1
comments.push({
fixed,
loc: {
start: {
line: line + startLineOffset,
column:
last(startLines).length + (startLineOffset ? 0 : column - 1),
offset: startOffset + offset,
},
end: {
line: line + endLineOffset - 1,
column: last(endLines).length + (endLineOffset ? 0 : column - 1),
offset: startOffset + endOffset,
},
},
origin: matched,
})
return fixed
},
),
})
}
12 changes: 6 additions & 6 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,21 @@ export const parseForESLint = (
}

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

normalizeJsxNode(node)
normalizeJsxNode(node, parent)

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

const rawText = node.value as string
const value = node.value as string

// fix #4
if (isComment(rawText)) {
if (isComment(value)) {
return
}

Expand All @@ -94,7 +94,7 @@ export const parseForESLint = (
let program: AST.Program | Linter.ESLintParseResult

try {
program = parser(rawText, options)
program = parser(value, options)
} catch (e) {
if (e instanceof SyntaxError) {
e.index += start
Expand Down
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
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { noJsxHtmlComments } from './no-jsx-html-comments'
import { noUnEscapedEntities } from './no-unescaped-entities'
import { noUnUsedExpressions } from './no-unused-expressions'

export { noJsxHtmlComments, noUnUsedExpressions }

export const rules = {
'no-jsx-html-comments': noJsxHtmlComments,
'no-unescaped-entities': noUnEscapedEntities,
'no-unused-expressions': noUnUsedExpressions,
}
47 changes: 24 additions & 23 deletions src/rules/no-jsx-html-comments.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Comment } from '../normalizer'

import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types'

import { Rule } from 'eslint'
Expand All @@ -12,7 +14,7 @@ export const noJsxHtmlComments: Rule.RuleModule = {
recommended: true,
},
messages: {
jdxHtmlComments: 'html style comments are invalid in jsx: {{ raw }}',
jsxHtmlComments: 'html style comments are invalid in jsx: {{ origin }}',
},
fixable: 'code',
schema: [],
Expand All @@ -33,30 +35,29 @@ export const noJsxHtmlComments: Rule.RuleModule = {
}

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,

if (invalidNode.data.inline) {
return
}

const comments = invalidNode.data.comments as Comment[]

comments.forEach(({ fixed, loc, origin }) =>
context.report({
messageId: 'jsxHtmlComments',
data: {
origin,
},
end: {
...end,
column: end.column - 1,
loc,
node,
fix(fixer) {
return fixer.replaceTextRange(
[loc.start.offset, loc.end.offset],
fixed,
)
},
},
fix(fixer) {
return fixer.replaceTextRange(
[start.offset, end.offset],
invalidNode.value as string,
)
},
})
}),
)
},
}
},
Expand Down
Loading

0 comments on commit dca8633

Please sign in to comment.