fix(scripts): find plugin record calls spelled with legacy octal escapes
A sloppy-mode .cjs or .js file can spell the name as '\141ppendPluginRecord', which TypeScript decodes to appendPluginRecord, but the prefilter only removed the backslash and skipped the file. The prefilter now decodes a legacy octal escape as TypeScript does, taking three digits only after 0 to 3, so the value stays within 0o377. The spec rejects two spellings that use three-digit octal escapes, and tests the decoder itself for the digit limit, which no spelling of the name, whose letters are all 0o1xx, can exercise.
This commit is contained in:
1 parent
8c6d23c128
commit
3a221c87e3
2 files changed
+24
-5
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
isAdmittedCaller,
|
||||
OWNER_FILE,
|
||||
readRepositorySources,
|
||||
unescaped,
|
||||
} from './verify-plugin-record-callers.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
@@ -56,6 +57,8 @@ describe('verify-plugin-record-callers', () => {
|
||||
['a hexadecimal escape in an element access', "S['\\x61ppendPluginRecord'](session, 'plugin:a', {})"],
|
||||
['a code point escape in an element access', "S['\\u{61}ppendPluginRecord'](session, 'plugin:a', {})"],
|
||||
['needless character escapes in an element access', "S['\\a\\ppendPluginRecord'](session, 'plugin:a', {})"],
|
||||
['a legacy octal escape in an element access', "S['\\141ppendPluginRecord'](session, 'plugin:a', {})"],
|
||||
['a three-digit legacy octal escape for the last character, beside a two-digit one that spells no reference', "S['appendPluginRecor\\144'](session, 'plugin:a\\61', {})"],
|
||||
['an escaped template element access', 'S[`\\x61ppendPluginRecord`](session, `plugin:a`, {})'],
|
||||
])('rejects %s in release package source', (_form, source) => {
|
||||
expect(checkPluginRecordCallers(corpus(`${source}\n`))).toEqual([{ file: RELEASE, line: 1, text: source }])
|
||||
@@ -79,6 +82,15 @@ describe('verify-plugin-record-callers', () => {
|
||||
it('reports nothing for escapes that spell no reference, including one that names no code point', () => {
|
||||
const source = "const path = 'C:\\\\temp\\\\appendPlugin'\nconst big = '\\u{110000}'\nconst newline = 'a\\nb'\n"
|
||||
expect(findPluginRecordReferences(RELEASE, source)).toEqual([])
|
||||
// A file the prefilter unescapes can still hold no reference: this escape spells no letter of the name.
|
||||
expect(findPluginRecordReferences('packages/core/x/src/legacy.cjs', "S['\\4141ppendPluginRecord']\n")).toEqual([])
|
||||
// The AST compares exact string values: " 0appendPluginRecord", as TypeScript decodes this, names nothing.
|
||||
expect(findPluginRecordReferences('packages/core/x/src/legacy.cjs', "S['\\400appendPluginRecord']\n")).toEqual([])
|
||||
})
|
||||
|
||||
it('unescapes legacy octal escapes as TypeScript decodes them, taking three digits only after 0 to 3', () => {
|
||||
// \400 exceeds 0o377, so it is \40 then a literal 0; \141 and \61 stay whole.
|
||||
expect([unescaped("'\\400'"), unescaped("'\\141\\61'"), unescaped("'\\0'"), unescaped("'\\777'")]).toEqual(["' 0'", "'a1'", "'\0'", "'?7'"])
|
||||
})
|
||||
|
||||
it('ignores comments, JSDoc links, longer strings, other identifiers, and wildcard re-exports', () => {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* (`export * from`) names no identifier and is not reported; code that calls the function through
|
||||
* it still names the function, and that reference is. A file is parsed when its text, or its text
|
||||
* with its escapes unescaped, contains the name, so a spelling that escapes part of it (`\u0061`,
|
||||
* `\x61`, `\u{61}`, `\a`, a line continuation) is still found.
|
||||
* `\x61`, `\u{61}`, `\141`, `\a`, a line continuation) is still found.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
@@ -47,19 +47,26 @@ const LINE_CONTINUATION = /\\(?:\r\n|[\n\r\u2028\u2029])/gu
|
||||
const HEX_ESCAPE = /\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2}))/gu
|
||||
|
||||
/**
|
||||
* The source as its escapes could spell it: line continuations dropped, hex escapes decoded, and
|
||||
* every other backslash removed. Text that the parser reads as the restricted name, in an
|
||||
* identifier or a string, contains the name here too.
|
||||
* A legacy octal escape, which a sloppy-mode script such as a `.cjs` file may use: up to three
|
||||
* octal digits, three only when the first is 0–3, so the value stays within 0o377.
|
||||
*/
|
||||
const OCTAL_ESCAPE = /\\([0-3][0-7]{0,2}|[4-7][0-7]?)/gu
|
||||
|
||||
/**
|
||||
* The source as its escapes could spell it: line continuations dropped, hex and legacy octal
|
||||
* escapes decoded, and every other backslash removed. Text that the parser reads as the
|
||||
* restricted name, in an identifier or a string, contains the name here too.
|
||||
* @param source - file contents.
|
||||
* @returns the unescaped text.
|
||||
*/
|
||||
function unescaped(source: string): string {
|
||||
export function unescaped(source: string): string {
|
||||
return source
|
||||
.replace(LINE_CONTINUATION, '')
|
||||
.replace(HEX_ESCAPE, (match: string, braced?: string, unit?: string, byte?: string) => {
|
||||
const code = Number.parseInt(`${braced ?? ''}${unit ?? ''}${byte ?? ''}`, 16)
|
||||
return code <= 0x10ffff ? String.fromCodePoint(code) : match
|
||||
})
|
||||
.replace(OCTAL_ESCAPE, (_match: string, digits: string) => String.fromCharCode(Number.parseInt(digits, 8)))
|
||||
.replaceAll('\\', '')
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user