Skip to content

Commit

Permalink
fix: add helper getPositionAt to fix incorrect loc (#293)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JounQin authored Mar 20, 2021
1 parent f5d288a commit 260a07e
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 33 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
]
},
"typeCoverage": {
"atLeast": 99.7,
"atLeast": 99.92,
"cache": true,
"detail": true,
"ignoreAsAssertion": true,
Expand Down
60 changes: 39 additions & 21 deletions packages/eslint-mdx/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/// <reference path="../typings.d.ts" />

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',
Expand Down Expand Up @@ -90,43 +96,55 @@ export const hasProperties = <T, P extends keyof T = keyof T>(
obj &&
properties.every(property => property in obj)

export const restoreNodeLocation = <T>(
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 = <T>(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<ValueOf<T>>) {
restoreNodeLocation(value, point)
}
}

if (!hasProperties<BaseNode>(node, ['loc', 'range'])) {
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),
},
},
})
Expand Down
14 changes: 10 additions & 4 deletions packages/eslint-mdx/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Node, Parent } from 'unist'

import {
arrayify,
getPositionAt,
hasProperties,
isJsxNode,
last,
Expand Down Expand Up @@ -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)
}
},
})
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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),
),
)
}
Expand Down
14 changes: 12 additions & 2 deletions packages/eslint-mdx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T[] | readonly T[]

export declare type ValueOf<T> = 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,
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin-mdx/src/processors/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin-mdx/src/processors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ESLintProcessor<
}

export interface ESLintMdxSettings {
'mdx/code-blocks': boolean
'mdx/code-blocks'?: boolean
'mdx/language-mapper'?: false | Record<string, string>
}

Expand Down
52 changes: 50 additions & 2 deletions test/__snapshots__/fixtures.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 []`;
Expand Down Expand Up @@ -169,6 +171,14 @@ Array [
"ruleId": "remark-lint-no-multiple-toplevel-headings",
"severity": 1,
},
Object {
"column": 7,
"line": 35,
"message": "\`'\` can be escaped with \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
"nodeType": "JSXText",
"ruleId": "react/no-unescaped-entities",
"severity": 1,
},
Object {
"column": 2,
"endColumn": 9,
Expand Down Expand Up @@ -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 \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
"nodeType": "JSXText",
"ruleId": "react/no-unescaped-entities",
"severity": 1,
},
Object {
"column": 5,
"line": 5,
"message": "\`'\` can be escaped with \`&apos;\`, \`&lsquo;\`, \`&#39;\`, \`&rsquo;\`.",
"nodeType": "JSXText",
"ruleId": "react/no-unescaped-entities",
"severity": 1,
},
]
`;

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

Expand All @@ -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 \`&gt;\`.",
"nodeType": "JSXText",
"ruleId": "react/no-unescaped-entities",
"severity": 1,
},
Object {
"column": 13,
"line": 5,
"message": "\`>\` can be escaped with \`&gt;\`.",
"nodeType": "JSXText",
"ruleId": "react/no-unescaped-entities",
"severity": 1,
},
]
`;

exports[`fixtures should match all snapshots: processor.mdx 1`] = `
Array [
Expand Down
22 changes: 22 additions & 0 deletions test/__snapshots__/helpers.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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,
}
`;
1 change: 1 addition & 0 deletions test/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/292.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Header

paragraph <div className="abc">content</div>

- - <div kind="docs-packages-vuetify-preset" story="page">
Vuetify preset
</div>
: some extra text describing the preset
15 changes: 14 additions & 1 deletion test/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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 <div>
<a href="link">content</a>
</div>
`.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({
Expand Down

0 comments on commit 260a07e

Please sign in to comment.