Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions lib/rules/no-dupe-characters-character-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import type {
AnyCharacterSet,
} from "regexpp/ast"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
import { createRule, defineRegexpVisitor, toCharSetSource } from "../utils"
import type { CharSet } from "refa"
import { JS } from "refa"
import type { ReadonlyFlags } from "regexp-ast-analysis"

/**
Expand Down Expand Up @@ -75,22 +74,6 @@ function groupingElements(
}
}

/**
* Returns a readable representation of the given char set.
*/
function charSetToReadableString(
charSet: CharSet,
flags: ReadonlyFlags,
): string {
return JS.toLiteral(
{
type: "Concatenation",
elements: [{ type: "CharacterClass", characters: charSet }],
},
{ flags },
).source
}

export default createRule("no-dupe-characters-character-class", {
meta: {
type: "suggestion",
Expand Down Expand Up @@ -155,10 +138,7 @@ export default createRule("no-dupe-characters-character-class", {
data: {
elementA: element.raw,
elementB: intersectElement.raw,
intersection: charSetToReadableString(
intersection,
flags,
),
intersection: toCharSetSource(intersection, flags),
},
})
}
Expand Down
11 changes: 5 additions & 6 deletions lib/rules/no-invisible-character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { RegExpContextForLiteral, RegExpContextForSource } from "../utils"
import {
createRule,
defineRegexpVisitor,
invisibleEscape,
isInvisible,
toCharSetSource,
} from "../utils"

export default createRule("no-invisible-character", {
Expand All @@ -30,6 +30,7 @@ export default createRule("no-invisible-character", {
*/
function createLiteralVisitor({
node,
flags,
getRegexpLocation,
getRegexpRange,
}: RegExpContextForLiteral): RegExpVisitor.Handlers {
Expand All @@ -39,9 +40,7 @@ export default createRule("no-invisible-character", {
return
}
if (cNode.raw.length === 1 && isInvisible(cNode.value)) {
const instead = invisibleEscape(
String.fromCodePoint(cNode.value),
)
const instead = toCharSetSource(cNode.value, flags)
context.report({
node,
loc: getRegexpLocation(cNode),
Expand All @@ -63,14 +62,14 @@ export default createRule("no-invisible-character", {
/**
* Verify a given string literal.
*/
function verifyString({ node }: RegExpContextForSource): void {
function verifyString({ node, flags }: RegExpContextForSource): void {
const text = sourceCode.getText(node)

let index = 0
for (const c of text) {
const cp = c.codePointAt(0)!
if (isInvisible(cp)) {
const instead = invisibleEscape(cp)
const instead = toCharSetSource(cp, flags)
const range: AST.Range = [
node.range![0] + index,
node.range![0] + index + c.length,
Expand Down
23 changes: 23 additions & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { ReadonlyFlags, ToCharSetElement } from "regexp-ast-analysis"
// eslint-disable-next-line no-restricted-imports -- Implement RegExpContext#toCharSet
import { toCharSet } from "regexp-ast-analysis"
import type { CharSet } from "refa"
import { JS } from "refa"
export * from "./unicode"

export type ToCharSet = (
Expand Down Expand Up @@ -750,6 +751,28 @@ export function quantToString(quant: Readonly<Quant>): string {
return value
}

/**
* Returns a regexp literal source of the given char set or char.
*/
export function toCharSetSource(
charSetOrChar: CharSet | number,
flags: ReadonlyFlags,
): string {
let charSet
if (typeof charSetOrChar === "number") {
charSet = JS.createCharSet([charSetOrChar], flags)
} else {
charSet = charSetOrChar
}
return JS.toLiteral(
{
type: "Concatenation",
elements: [{ type: "CharacterClass", characters: charSet }],
},
{ flags },
).source
}

/* eslint-disable complexity -- X( */
/**
* Returns whether the concatenation of the two string might create new escape
Expand Down
41 changes: 0 additions & 41 deletions lib/utils/unicode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,44 +176,3 @@ export function isInvisible(codePoint: number): boolean {
codePoint === CP_BRAILLE_PATTERN_BLANK
)
}

/**
* Returns a string with invisible characters converted to escape characters.
*/
export function invisibleEscape(val: string | number): string {
let result = ""

for (const cp of typeof val === "number" ? [val] : codePoints(val)) {
if (cp !== CP_SPACE && isInvisible(cp)) {
if (cp === CP_TAB) {
result += "\\t"
} else if (cp === CP_LF) {
result += "\\r"
} else if (cp === CP_CR) {
result += "\\n"
} else if (cp === CP_VT) {
result += "\\v"
} else if (cp === CP_FF) {
result += "\\f"
} else {
result += `\\u${`${cp.toString(16)}`.padStart(4, "0")}`
}
} else {
result += String.fromCodePoint(cp)
}
}
return result
}

/**
* String to code points
*/
function* codePoints(s: string) {
for (let i = 0; i < s.length; i += 1) {
const cp = s.codePointAt(i)!
yield cp
if (cp >= 0x10000) {
i += 1
}
}
}
22 changes: 11 additions & 11 deletions tests/lib/rules/no-invisible-character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ tester.run("no-invisible-character", rule as any, {
invalid: [
{
code: "/\u00a0/",
output: "/\\u00a0/",
errors: ["Unexpected invisible character. Use '\\u00a0' instead."],
output: "/\\xa0/",
errors: ["Unexpected invisible character. Use '\\xa0' instead."],
},
{
code: "/[\t]/",
Expand All @@ -35,10 +35,10 @@ tester.run("no-invisible-character", rule as any, {
code:
"/[\t\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff\u0085\u200b]/",
output:
"/[\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b]/",
"/[\\t\xa0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\x85\\u200b]/",
errors: [
"Unexpected invisible character. Use '\\t' instead.",
"Unexpected invisible character. Use '\\u00a0' instead.",
"Unexpected invisible character. Use '\\xa0' instead.",
"Unexpected invisible character. Use '\\u1680' instead.",
"Unexpected invisible character. Use '\\u180e' instead.",
"Unexpected invisible character. Use '\\u2000' instead.",
Expand All @@ -56,17 +56,17 @@ tester.run("no-invisible-character", rule as any, {
"Unexpected invisible character. Use '\\u205f' instead.",
"Unexpected invisible character. Use '\\u3000' instead.",
"Unexpected invisible character. Use '\\ufeff' instead.",
"Unexpected invisible character. Use '\\u0085' instead.",
"Unexpected invisible character. Use '\\x85' instead.",
"Unexpected invisible character. Use '\\u200b' instead.",
],
},
{
code:
"/[\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b]/",
output:
"/[\\t\\u00a0\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\ufeff\\u0085\\u200b]/",
"/[\\t\\xa0\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000\\ufeff\\x85\\u200b]/",
errors: [
"Unexpected invisible character. Use '\\u00a0' instead.",
"Unexpected invisible character. Use '\\xa0' instead.",
"Unexpected invisible character. Use '\\u180e' instead.",
"Unexpected invisible character. Use '\\u2001' instead.",
"Unexpected invisible character. Use '\\u2003' instead.",
Expand All @@ -75,17 +75,17 @@ tester.run("no-invisible-character", rule as any, {
"Unexpected invisible character. Use '\\u2009' instead.",
"Unexpected invisible character. Use '\\u202f' instead.",
"Unexpected invisible character. Use '\\u3000' instead.",
"Unexpected invisible character. Use '\\u0085' instead.",
"Unexpected invisible character. Use '\\x85' instead.",
],
},
{
code:
"new RegExp('\t\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff\u0085\u200b')",
output:
"new RegExp('\\t\u00a0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\u0085\\u200b')",
"new RegExp('\\t\xa0\\u1680\u180e\\u2000\u2001\\u2002\u2003\\u2004\u2005\\u2006\u2007\\u2008\u2009\\u200a\u202f\\u205f\u3000\\ufeff\x85\\u200b')",
errors: [
"Unexpected invisible character. Use '\\t' instead.",
"Unexpected invisible character. Use '\\u00a0' instead.",
"Unexpected invisible character. Use '\\xa0' instead.",
"Unexpected invisible character. Use '\\u1680' instead.",
"Unexpected invisible character. Use '\\u180e' instead.",
"Unexpected invisible character. Use '\\u2000' instead.",
Expand All @@ -103,7 +103,7 @@ tester.run("no-invisible-character", rule as any, {
"Unexpected invisible character. Use '\\u205f' instead.",
"Unexpected invisible character. Use '\\u3000' instead.",
"Unexpected invisible character. Use '\\ufeff' instead.",
"Unexpected invisible character. Use '\\u0085' instead.",
"Unexpected invisible character. Use '\\x85' instead.",
"Unexpected invisible character. Use '\\u200b' instead.",
],
},
Expand Down
36 changes: 36 additions & 0 deletions tests/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from "assert"
import { toCharSetSource } from "../../../lib/utils"

describe("toCharSetSource", () => {
for (const c of "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789") {
const cp = c.codePointAt(0)!
it(`0x${cp.toString(16)} to ${c}`, () => {
assert.strictEqual(toCharSetSource(cp, {}), c)
})
}
it(`0x9 to \\t`, () => {
assert.strictEqual(toCharSetSource(9, {}), "\\t")
})
it(`0xA to \\n`, () => {
assert.strictEqual(toCharSetSource(10, {}), "\\n")
})
it(`0xC to \\f`, () => {
assert.strictEqual(toCharSetSource(12, {}), "\\f")
})
it(`0xD to \\n`, () => {
assert.strictEqual(toCharSetSource(13, {}), "\\r")
})
})

describe("toCharSetSource with invisible chars", () => {
const str =
"\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" +
"\u0085\u200b\u200c\u200d\u200e\u200f\u2800"

for (const c of str) {
const cp = c.codePointAt(0)!
it(`0x${cp.toString(16)}`, () => {
assert.notStrictEqual(toCharSetSource(cp, {}), c)
})
}
})
8 changes: 4 additions & 4 deletions tests/lib/utils/unicode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from "assert"
import { toCharSetSource } from "../../../lib/utils"
import {
isSpace,
isInvisible,
invisibleEscape,
CP_NEL,
CP_ZWSP,
} from "../../../lib/utils/unicode"
Expand All @@ -12,13 +12,13 @@ const SPACES =

describe("isSpace", () => {
for (const c of SPACES) {
it(`${invisibleEscape(c)} is space`, () => {
it(`${toCharSetSource(c.codePointAt(0)!, {})} is space`, () => {
assert.ok(isSpace(c.codePointAt(0)!))
})
}

for (const c of [CP_NEL, CP_ZWSP]) {
it(`${invisibleEscape(c)} is not space`, () => {
it(`${toCharSetSource(c, {})} is not space`, () => {
assert.ok(!isSpace(c))
})
}
Expand All @@ -28,7 +28,7 @@ describe("isInvisible", () => {
const str = `${SPACES}\u0085\u200b\u200c\u200d\u200e\u200f\u2800`

for (const c of str) {
it(`${invisibleEscape(c)} is invisible`, () => {
it(`${toCharSetSource(c.codePointAt(0)!, {})} is invisible`, () => {
assert.ok(isInvisible(c.codePointAt(0)!))
})
}
Expand Down