@masknet/eslint-plugin
Version:
eslint plugin for masknet
140 lines (130 loc) • 3.93 kB
text/typescript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { TSESTree } from '@typescript-eslint/types'
import type { ReportFixFunction, RuleListener } from '@typescript-eslint/utils/ts-eslint'
import { createRule } from '../../rule.js'
// https://unicode.org/reports/tr18
// https://unicode.org/reports/tr51
const BUILT_PATTERN = /\P{ASCII}/u
export interface Options {
pattern: RegExp['source']
flags: RegExp['flags']
only: 'code' | 'comment'
}
interface ResolvedOptions {
only?: Options['only']
pattern: RegExp
}
export default createRule({
name: 'unicode/specific-set',
meta: {
type: 'problem',
fixable: 'code',
docs: {
description: 'Limit the range of literal characters',
},
schema: [
{
type: 'object',
properties: {
pattern: { type: 'string' },
flags: { type: 'string' },
only: { type: 'string', enum: ['code', 'comment'] },
},
additionalProperties: false,
},
],
messages: {
illegal: 'Illegal character detected',
},
},
resolveOptions(options?: Partial<Options>): ResolvedOptions {
const pattern = options?.pattern ? new RegExp(options.pattern, options.flags ?? 'u') : BUILT_PATTERN
return { pattern, only: options?.only }
},
create(context, { pattern, only }: ResolvedOptions) {
return makeProgramListener(pattern, (node, kind) => {
if (only && only !== kind) return
const matcher = new RegExp(pattern.source, 'gu')
const fix = getFixer(node, matcher)
context.report({ node, messageId: 'illegal', fix })
})
},
})
export function makeProgramListener(
pattern: RegExp,
onReport: (node: TSESTree.Token, kind: string) => void,
): RuleListener {
return {
Program(program: TSESTree.Program) {
for (const token of program.tokens ?? []) {
const value = getValue(token)
if (value === false) continue
if (!pattern.test(value)) continue
onReport(token, 'code')
}
for (const comment of program.comments ?? []) {
if (!pattern.test(comment.value)) continue
onReport(comment, 'comment')
}
},
}
}
export function getFixer(token: TSESTree.Token, pattern: RegExp): ReportFixFunction | undefined {
switch (token.type) {
case 'String':
case 'Template': {
return (fixer) => {
const prefix = token.value.slice(0, 1)
const suffix = token.value.slice(-1)
const modified = escape(token.value.slice(1, -1), pattern)
return fixer.replaceText(token, `${prefix}${modified}${suffix}`)
}
}
case 'JSXText': {
return (fixer) => {
const modified = token.value.replace(pattern, (match) => `&#x${toString(match.codePointAt(0)!)};`)
return fixer.replaceText(token, modified)
}
}
case 'RegularExpression': {
return (fixer) => {
const flags = new Set(token.regex.flags)
flags.add('u')
const re = new RegExp(escape(token.regex.pattern, pattern), [...flags].join(''))
return fixer.replaceText(token, re.toString())
}
}
}
return
}
export function escape(input: string, pattern: RegExp) {
return input.replace(pattern, (match) => {
const point = match.codePointAt(0)!
return point > 65_535 ? `\\u{${toString(point)}}` : `\\u${toString(point)}`
})
}
function toString(point: number) {
return point.toString(16).padStart(4, '0').toUpperCase()
}
function getValue(token: TSESTree.Token) {
switch (token.type) {
case 'String':
case 'Template': {
return token.value.slice(1, -1)
}
case 'Identifier': {
if (token.value.startsWith('#')) {
return token.value.slice(1)
}
return token.value
}
case 'RegularExpression': {
return token.regex.pattern
}
case 'JSXText':
case 'JSXIdentifier': {
return token.value
}
}
return false
}