Skip to content

Commit 260a07e

Browse files
authored
fix: add helper getPositionAt to fix incorrect loc (#293)
* fix: add helper getPositionAt close #292 * refactor: simplify offset which is always start actually * chore: all settings are optional * refactor: use a better and cleaner way to hack * chore: run typecov and improve types
1 parent f5d288a commit 260a07e

File tree

12 files changed

+160
-33
lines changed

12 files changed

+160
-33
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = {
3333
// related to https://github.com/eslint/eslint/issues/14207
3434
rules: {
3535
'prettier/prettier': 0,
36+
'react/no-unescaped-entities': 1,
3637
'unicorn/filename-case': 0,
3738
},
3839
settings: {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
]
9595
},
9696
"typeCoverage": {
97-
"atLeast": 99.7,
97+
"atLeast": 99.92,
9898
"cache": true,
9999
"detail": true,
100100
"ignoreAsAssertion": true,

packages/eslint-mdx/src/helpers.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/// <reference path="../typings.d.ts" />
22

33
import type { Linter } from 'eslint'
4-
import type { SourceLocation } from 'estree'
5-
import type { Position } from 'unist'
4+
import type { Position as ESPosition, SourceLocation } from 'estree'
5+
import type { Point, Position } from 'unist'
66

7-
import type { Arrayable, JsxNode, ParserFn, ParserOptions } from './types'
7+
import type {
8+
Arrayable,
9+
JsxNode,
10+
ParserFn,
11+
ParserOptions,
12+
ValueOf,
13+
} from './types'
814

915
export const FALLBACK_PARSERS = [
1016
'@typescript-eslint/parser',
@@ -90,43 +96,55 @@ export const hasProperties = <T, P extends keyof T = keyof T>(
9096
obj &&
9197
properties.every(property => property in obj)
9298

93-
export const restoreNodeLocation = <T>(
94-
node: T,
95-
startLine: number,
96-
offset: number,
97-
): T => {
99+
// fix #292
100+
export const getPositionAt = (code: string, offset: number): ESPosition => {
101+
let currOffset = 0
102+
103+
for (const [index, { length }] of code.split('\n').entries()) {
104+
const line = index + 1
105+
const nextOffset = currOffset + length
106+
107+
if (nextOffset >= offset) {
108+
return {
109+
line,
110+
column: offset - currOffset,
111+
}
112+
}
113+
114+
currOffset = nextOffset + 1 // add a line break `\n` offset
115+
}
116+
}
117+
118+
export const restoreNodeLocation = <T>(node: T, point: Point): T => {
98119
if (node && typeof node === 'object') {
99-
for (const value of Object.values(node)) {
100-
restoreNodeLocation(value, startLine, offset)
120+
for (const value of Object.values(node) as Array<ValueOf<T>>) {
121+
restoreNodeLocation(value, point)
101122
}
102123
}
103124

104125
if (!hasProperties<BaseNode>(node, ['loc', 'range'])) {
105126
return node
106127
}
107128

108-
const {
129+
let {
109130
loc: { start: startLoc, end: endLoc },
110-
range,
131+
range: [start, end],
111132
} = node
112-
const start = range[0] + offset
113-
const end = range[1] + offset
114133

115-
const restoredStartLine = startLine + startLoc.line
116-
const restoredEndLine = startLine + endLoc.line
134+
const range = [(start += point.offset), (end += point.offset)] as const
117135

118136
return Object.assign(node, {
119137
start,
120138
end,
121-
range: [start, end],
139+
range,
122140
loc: {
123141
start: {
124-
line: restoredStartLine,
125-
column: startLoc.column + (restoredStartLine === 1 ? offset : 0),
142+
line: point.line + startLoc.line,
143+
column: startLoc.column + (startLoc.line === 1 ? point.column : 0),
126144
},
127145
end: {
128-
line: restoredEndLine,
129-
column: endLoc.column + (restoredEndLine === 1 ? offset : 0),
146+
line: point.line + endLoc.line,
147+
column: endLoc.column + (endLoc.line === 1 ? point.column : 0),
130148
},
131149
},
132150
})

packages/eslint-mdx/src/parser.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Node, Parent } from 'unist'
99

1010
import {
1111
arrayify,
12+
getPositionAt,
1213
hasProperties,
1314
isJsxNode,
1415
last,
@@ -193,7 +194,7 @@ export class Parser {
193194
for (const normalizedNode of arrayify(
194195
this.normalizeJsxNode(node, parent, options),
195196
)) {
196-
this._nodeToAst(normalizedNode, options)
197+
this._nodeToAst(code, normalizedNode, options)
197198
}
198199
},
199200
})
@@ -333,7 +334,7 @@ export class Parser {
333334
}
334335

335336
// @internal
336-
private _nodeToAst(node: Node, options: ParserOptions) {
337+
private _nodeToAst(code: string, node: Node, options: ParserOptions) {
337338
if (node.data && node.data.jsxType === 'JSXElementWithHTMLComments') {
338339
this._services.JSXElementsWithHTMLComments.push(node)
339340
}
@@ -371,13 +372,18 @@ export class Parser {
371372
throw e
372373
}
373374

374-
const offset = start - program.range[0]
375+
const startPoint = {
376+
line: startLine,
377+
// #279 related
378+
column: getPositionAt(code, start).column,
379+
offset: start,
380+
}
375381

376382
for (const prop of AST_PROPS)
377383
this._ast[prop].push(
378384
// ts doesn't understand the mixed type
379385
...program[prop].map((item: never) =>
380-
restoreNodeLocation(item, startLine, offset),
386+
restoreNodeLocation(item, startPoint),
381387
),
382388
)
383389
}

packages/eslint-mdx/src/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@ import type { JSXElement, JSXFragment } from '@babel/types'
22
import type { AST, Linter } from 'eslint'
33
import type { Node, Parent, Point } from 'unist'
44

5-
export type JsxNode = (JSXElement | JSXFragment) & { range: [number, number] }
6-
75
export type Arrayable<T> = T[] | readonly T[]
86

7+
export declare type ValueOf<T> = T extends {
8+
[key: string]: infer M
9+
}
10+
? M
11+
: T extends {
12+
[key: number]: infer N
13+
}
14+
? N
15+
: never
16+
17+
export type JsxNode = (JSXElement | JSXFragment) & { range: [number, number] }
18+
919
export type ParserFn = (
1020
code: string,
1121
options: Linter.ParserOptions,

packages/eslint-plugin-mdx/src/processors/markdown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ function preprocess(text: string, filename: string): ESLinterProcessorFile[] {
242242

243243
traverse(ast, {
244244
code(node, parent) {
245-
const comments = []
245+
const comments: string[] = []
246246

247247
if (node.lang) {
248248
let index = parent.children.indexOf(node) - 1

packages/eslint-plugin-mdx/src/processors/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ESLintProcessor<
1818
}
1919

2020
export interface ESLintMdxSettings {
21-
'mdx/code-blocks': boolean
21+
'mdx/code-blocks'?: boolean
2222
'mdx/language-mapper'?: false | Record<string, string>
2323
}
2424

test/__snapshots__/fixtures.test.ts.snap

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ Array [
109109

110110
exports[`fixtures should match all snapshots: 287.mdx 1`] = `Array []`;
111111

112+
exports[`fixtures should match all snapshots: 292.mdx 1`] = `Array []`;
113+
112114
exports[`fixtures should match all snapshots: adjacent.mdx 1`] = `Array []`;
113115

114116
exports[`fixtures should match all snapshots: basic.mdx 1`] = `Array []`;
@@ -169,6 +171,14 @@ Array [
169171
"ruleId": "remark-lint-no-multiple-toplevel-headings",
170172
"severity": 1,
171173
},
174+
Object {
175+
"column": 7,
176+
"line": 35,
177+
"message": "\`'\` can be escaped with \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
178+
"nodeType": "JSXText",
179+
"ruleId": "react/no-unescaped-entities",
180+
"severity": 1,
181+
},
172182
Object {
173183
"column": 2,
174184
"endColumn": 9,
@@ -221,7 +231,26 @@ Array [
221231
]
222232
`;
223233

224-
exports[`fixtures should match all snapshots: details.mdx 1`] = `Array []`;
234+
exports[`fixtures should match all snapshots: details.mdx 1`] = `
235+
Array [
236+
Object {
237+
"column": 290,
238+
"line": 3,
239+
"message": "\`'\` can be escaped with \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
240+
"nodeType": "JSXText",
241+
"ruleId": "react/no-unescaped-entities",
242+
"severity": 1,
243+
},
244+
Object {
245+
"column": 5,
246+
"line": 5,
247+
"message": "\`'\` can be escaped with \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
248+
"nodeType": "JSXText",
249+
"ruleId": "react/no-unescaped-entities",
250+
"severity": 1,
251+
},
252+
]
253+
`;
225254

226255
exports[`fixtures should match all snapshots: jsx-in-list.mdx 1`] = `Array []`;
227256

@@ -231,7 +260,26 @@ exports[`fixtures should match all snapshots: markdown.md 1`] = `Array []`;
231260

232261
exports[`fixtures should match all snapshots: no-jsx-html-comments.mdx 1`] = `Array []`;
233262

234-
exports[`fixtures should match all snapshots: no-unescaped-entities.mdx 1`] = `Array []`;
263+
exports[`fixtures should match all snapshots: no-unescaped-entities.mdx 1`] = `
264+
Array [
265+
Object {
266+
"column": 8,
267+
"line": 2,
268+
"message": "\`>\` can be escaped with \`&gt;\`.",
269+
"nodeType": "JSXText",
270+
"ruleId": "react/no-unescaped-entities",
271+
"severity": 1,
272+
},
273+
Object {
274+
"column": 13,
275+
"line": 5,
276+
"message": "\`>\` can be escaped with \`&gt;\`.",
277+
"nodeType": "JSXText",
278+
"ruleId": "react/no-unescaped-entities",
279+
"severity": 1,
280+
},
281+
]
282+
`;
235283

236284
exports[`fixtures should match all snapshots: processor.mdx 1`] = `
237285
Array [
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Helpers should get correct loc range range 1`] = `
4+
Object {
5+
"column": 2,
6+
"line": 1,
7+
}
8+
`;
9+
10+
exports[`Helpers should get correct loc range range 2`] = `
11+
Object {
12+
"column": 13,
13+
"line": 4,
14+
}
15+
`;
16+
17+
exports[`Helpers should get correct loc range range 3`] = `
18+
Object {
19+
"column": 19,
20+
"line": 4,
21+
}
22+
`;

test/fixtures.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const getCli = (lintCodeBlocks = false) =>
1515
extends: ['plugin:mdx/recommended'],
1616
plugins: ['react', 'unicorn', 'prettier'],
1717
rules: {
18+
'react/no-unescaped-entities': 1,
1819
'unicorn/prefer-spread': 2,
1920
},
2021
overrides: lintCodeBlocks

0 commit comments

Comments
 (0)