UNPKG

@masknet/eslint-plugin

Version:
140 lines (130 loc) 3.93 kB
/* 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 }