diff --git a/.eslintrc.js b/.eslintrc.js
index 22b36bec..a5073c36 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -33,6 +33,7 @@ module.exports = {
// related to /~https://github.com/eslint/eslint/issues/14207
rules: {
'prettier/prettier': 0,
+ 'react/no-unescaped-entities': 1,
'unicorn/filename-case': 0,
},
settings: {
diff --git a/package.json b/package.json
index 36f6ba47..b5346a68 100644
--- a/package.json
+++ b/package.json
@@ -94,7 +94,7 @@
]
},
"typeCoverage": {
- "atLeast": 99.7,
+ "atLeast": 99.92,
"cache": true,
"detail": true,
"ignoreAsAssertion": true,
diff --git a/packages/eslint-mdx/src/helpers.ts b/packages/eslint-mdx/src/helpers.ts
index 2d733982..cc68f031 100644
--- a/packages/eslint-mdx/src/helpers.ts
+++ b/packages/eslint-mdx/src/helpers.ts
@@ -1,10 +1,16 @@
///
import type { Linter } from 'eslint'
-import type { SourceLocation } from 'estree'
-import type { Position } from 'unist'
+import type { Position as ESPosition, SourceLocation } from 'estree'
+import type { Point, Position } from 'unist'
-import type { Arrayable, JsxNode, ParserFn, ParserOptions } from './types'
+import type {
+ Arrayable,
+ JsxNode,
+ ParserFn,
+ ParserOptions,
+ ValueOf,
+} from './types'
export const FALLBACK_PARSERS = [
'@typescript-eslint/parser',
@@ -90,14 +96,29 @@ export const hasProperties = (
obj &&
properties.every(property => property in obj)
-export const restoreNodeLocation = (
- node: T,
- startLine: number,
- offset: number,
-): T => {
+// fix #292
+export const getPositionAt = (code: string, offset: number): ESPosition => {
+ let currOffset = 0
+
+ for (const [index, { length }] of code.split('\n').entries()) {
+ const line = index + 1
+ const nextOffset = currOffset + length
+
+ if (nextOffset >= offset) {
+ return {
+ line,
+ column: offset - currOffset,
+ }
+ }
+
+ currOffset = nextOffset + 1 // add a line break `\n` offset
+ }
+}
+
+export const restoreNodeLocation = (node: T, point: Point): T => {
if (node && typeof node === 'object') {
- for (const value of Object.values(node)) {
- restoreNodeLocation(value, startLine, offset)
+ for (const value of Object.values(node) as Array>) {
+ restoreNodeLocation(value, point)
}
}
@@ -105,28 +126,25 @@ export const restoreNodeLocation = (
return node
}
- const {
+ let {
loc: { start: startLoc, end: endLoc },
- range,
+ range: [start, end],
} = node
- const start = range[0] + offset
- const end = range[1] + offset
- const restoredStartLine = startLine + startLoc.line
- const restoredEndLine = startLine + endLoc.line
+ const range = [(start += point.offset), (end += point.offset)] as const
return Object.assign(node, {
start,
end,
- range: [start, end],
+ range,
loc: {
start: {
- line: restoredStartLine,
- column: startLoc.column + (restoredStartLine === 1 ? offset : 0),
+ line: point.line + startLoc.line,
+ column: startLoc.column + (startLoc.line === 1 ? point.column : 0),
},
end: {
- line: restoredEndLine,
- column: endLoc.column + (restoredEndLine === 1 ? offset : 0),
+ line: point.line + endLoc.line,
+ column: endLoc.column + (endLoc.line === 1 ? point.column : 0),
},
},
})
diff --git a/packages/eslint-mdx/src/parser.ts b/packages/eslint-mdx/src/parser.ts
index 8eeb4ee2..56a0e04d 100644
--- a/packages/eslint-mdx/src/parser.ts
+++ b/packages/eslint-mdx/src/parser.ts
@@ -9,6 +9,7 @@ import type { Node, Parent } from 'unist'
import {
arrayify,
+ getPositionAt,
hasProperties,
isJsxNode,
last,
@@ -193,7 +194,7 @@ export class Parser {
for (const normalizedNode of arrayify(
this.normalizeJsxNode(node, parent, options),
)) {
- this._nodeToAst(normalizedNode, options)
+ this._nodeToAst(code, normalizedNode, options)
}
},
})
@@ -333,7 +334,7 @@ export class Parser {
}
// @internal
- private _nodeToAst(node: Node, options: ParserOptions) {
+ private _nodeToAst(code: string, node: Node, options: ParserOptions) {
if (node.data && node.data.jsxType === 'JSXElementWithHTMLComments') {
this._services.JSXElementsWithHTMLComments.push(node)
}
@@ -371,13 +372,18 @@ export class Parser {
throw e
}
- const offset = start - program.range[0]
+ const startPoint = {
+ line: startLine,
+ // #279 related
+ column: getPositionAt(code, start).column,
+ offset: start,
+ }
for (const prop of AST_PROPS)
this._ast[prop].push(
// ts doesn't understand the mixed type
...program[prop].map((item: never) =>
- restoreNodeLocation(item, startLine, offset),
+ restoreNodeLocation(item, startPoint),
),
)
}
diff --git a/packages/eslint-mdx/src/types.ts b/packages/eslint-mdx/src/types.ts
index a87c9433..c39c7e16 100644
--- a/packages/eslint-mdx/src/types.ts
+++ b/packages/eslint-mdx/src/types.ts
@@ -2,10 +2,20 @@ import type { JSXElement, JSXFragment } from '@babel/types'
import type { AST, Linter } from 'eslint'
import type { Node, Parent, Point } from 'unist'
-export type JsxNode = (JSXElement | JSXFragment) & { range: [number, number] }
-
export type Arrayable = T[] | readonly T[]
+export declare type ValueOf = T extends {
+ [key: string]: infer M
+}
+ ? M
+ : T extends {
+ [key: number]: infer N
+ }
+ ? N
+ : never
+
+export type JsxNode = (JSXElement | JSXFragment) & { range: [number, number] }
+
export type ParserFn = (
code: string,
options: Linter.ParserOptions,
diff --git a/packages/eslint-plugin-mdx/src/processors/markdown.ts b/packages/eslint-plugin-mdx/src/processors/markdown.ts
index 2cb6977e..86ac6b0e 100644
--- a/packages/eslint-plugin-mdx/src/processors/markdown.ts
+++ b/packages/eslint-plugin-mdx/src/processors/markdown.ts
@@ -242,7 +242,7 @@ function preprocess(text: string, filename: string): ESLinterProcessorFile[] {
traverse(ast, {
code(node, parent) {
- const comments = []
+ const comments: string[] = []
if (node.lang) {
let index = parent.children.indexOf(node) - 1
diff --git a/packages/eslint-plugin-mdx/src/processors/types.ts b/packages/eslint-plugin-mdx/src/processors/types.ts
index a569978c..75fc102a 100644
--- a/packages/eslint-plugin-mdx/src/processors/types.ts
+++ b/packages/eslint-plugin-mdx/src/processors/types.ts
@@ -18,7 +18,7 @@ export interface ESLintProcessor<
}
export interface ESLintMdxSettings {
- 'mdx/code-blocks': boolean
+ 'mdx/code-blocks'?: boolean
'mdx/language-mapper'?: false | Record
}
diff --git a/test/__snapshots__/fixtures.test.ts.snap b/test/__snapshots__/fixtures.test.ts.snap
index 97f6422a..494296f5 100644
--- a/test/__snapshots__/fixtures.test.ts.snap
+++ b/test/__snapshots__/fixtures.test.ts.snap
@@ -109,6 +109,8 @@ Array [
exports[`fixtures should match all snapshots: 287.mdx 1`] = `Array []`;
+exports[`fixtures should match all snapshots: 292.mdx 1`] = `Array []`;
+
exports[`fixtures should match all snapshots: adjacent.mdx 1`] = `Array []`;
exports[`fixtures should match all snapshots: basic.mdx 1`] = `Array []`;
@@ -169,6 +171,14 @@ Array [
"ruleId": "remark-lint-no-multiple-toplevel-headings",
"severity": 1,
},
+ Object {
+ "column": 7,
+ "line": 35,
+ "message": "\`'\` can be escaped with \`'\`, \`‘\`, \`'\`, \`’\`.",
+ "nodeType": "JSXText",
+ "ruleId": "react/no-unescaped-entities",
+ "severity": 1,
+ },
Object {
"column": 2,
"endColumn": 9,
@@ -221,7 +231,26 @@ Array [
]
`;
-exports[`fixtures should match all snapshots: details.mdx 1`] = `Array []`;
+exports[`fixtures should match all snapshots: details.mdx 1`] = `
+Array [
+ Object {
+ "column": 290,
+ "line": 3,
+ "message": "\`'\` can be escaped with \`'\`, \`‘\`, \`'\`, \`’\`.",
+ "nodeType": "JSXText",
+ "ruleId": "react/no-unescaped-entities",
+ "severity": 1,
+ },
+ Object {
+ "column": 5,
+ "line": 5,
+ "message": "\`'\` can be escaped with \`'\`, \`‘\`, \`'\`, \`’\`.",
+ "nodeType": "JSXText",
+ "ruleId": "react/no-unescaped-entities",
+ "severity": 1,
+ },
+]
+`;
exports[`fixtures should match all snapshots: jsx-in-list.mdx 1`] = `Array []`;
@@ -231,7 +260,26 @@ exports[`fixtures should match all snapshots: markdown.md 1`] = `Array []`;
exports[`fixtures should match all snapshots: no-jsx-html-comments.mdx 1`] = `Array []`;
-exports[`fixtures should match all snapshots: no-unescaped-entities.mdx 1`] = `Array []`;
+exports[`fixtures should match all snapshots: no-unescaped-entities.mdx 1`] = `
+Array [
+ Object {
+ "column": 8,
+ "line": 2,
+ "message": "\`>\` can be escaped with \`>\`.",
+ "nodeType": "JSXText",
+ "ruleId": "react/no-unescaped-entities",
+ "severity": 1,
+ },
+ Object {
+ "column": 13,
+ "line": 5,
+ "message": "\`>\` can be escaped with \`>\`.",
+ "nodeType": "JSXText",
+ "ruleId": "react/no-unescaped-entities",
+ "severity": 1,
+ },
+]
+`;
exports[`fixtures should match all snapshots: processor.mdx 1`] = `
Array [
diff --git a/test/__snapshots__/helpers.test.ts.snap b/test/__snapshots__/helpers.test.ts.snap
new file mode 100644
index 00000000..1b35ebbc
--- /dev/null
+++ b/test/__snapshots__/helpers.test.ts.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Helpers should get correct loc range range 1`] = `
+Object {
+ "column": 2,
+ "line": 1,
+}
+`;
+
+exports[`Helpers should get correct loc range range 2`] = `
+Object {
+ "column": 13,
+ "line": 4,
+}
+`;
+
+exports[`Helpers should get correct loc range range 3`] = `
+Object {
+ "column": 19,
+ "line": 4,
+}
+`;
diff --git a/test/fixtures.test.ts b/test/fixtures.test.ts
index 12eb8039..8fe9aa99 100644
--- a/test/fixtures.test.ts
+++ b/test/fixtures.test.ts
@@ -15,6 +15,7 @@ const getCli = (lintCodeBlocks = false) =>
extends: ['plugin:mdx/recommended'],
plugins: ['react', 'unicorn', 'prettier'],
rules: {
+ 'react/no-unescaped-entities': 1,
'unicorn/prefer-spread': 2,
},
overrides: lintCodeBlocks
diff --git a/test/fixtures/292.mdx b/test/fixtures/292.mdx
new file mode 100644
index 00000000..963fa36d
--- /dev/null
+++ b/test/fixtures/292.mdx
@@ -0,0 +1,8 @@
+# Header
+
+paragraph content
+
+- -
+ Vuetify preset
+
+ : some extra text describing the preset
diff --git a/test/helpers.test.ts b/test/helpers.test.ts
index db9fc580..0c22657e 100644
--- a/test/helpers.test.ts
+++ b/test/helpers.test.ts
@@ -1,6 +1,6 @@
import path from 'path'
-import { arrayify } from 'eslint-mdx'
+import { arrayify, getPositionAt } from 'eslint-mdx'
import { getGlobals, getShortLang, requirePkg } from 'eslint-plugin-mdx'
describe('Helpers', () => {
@@ -18,6 +18,19 @@ describe('Helpers', () => {
expect(getShortLang('4.Markdown', { markdown: 'mkdn' })).toBe('mkdn')
})
+ it('should get correct loc range range', () => {
+ const code = `
+# Header
+
+- jsx in list
+ `.trim()
+ expect(getPositionAt(code, code.indexOf('Header'))).toMatchSnapshot()
+ expect(getPositionAt(code, code.indexOf('link'))).toMatchSnapshot()
+ expect(getPositionAt(code, code.indexOf('content'))).toMatchSnapshot()
+ })
+
it('should resolve globals correctly', () => {
expect(getGlobals({})).toEqual({})
expect(getGlobals(['a', 'b'])).toEqual({