|
740 | 740 | * index matches the bracket-access literal, and fires when the result is called. |
741 | 741 | * `unsafe {}` suppresses. |
742 | 742 | * |
| 743 | + * SYN070 A SYN-guarded global (`eval`, `fetch`, `Function`, etc.) appears at index N in an |
| 744 | + * inline array literal that is immediately accessed via `.at(N)` and called in a fn |
| 745 | + * body — `[eval].at(0)(code)`, `[x, fetch].at(1)(url)`. SYN069 closes the bracket- |
| 746 | + * notation gap (`[eval][0](...)`); `Array.prototype.at()` is the modern equivalent and |
| 747 | + * bypasses SYN069 because the token sequence after `]` is `.at(N)(` not `[N](`. All |
| 748 | + * per-ident checks still miss the global (not in call position inside `[...]`); all |
| 749 | + * alias-binding checks miss it (no binding). SYN070 closes the gap: a pre-pass finds |
| 750 | + * guarded globals in array-element position, confirms the index matches the `.at()` |
| 751 | + * argument, and fires when the result is called. `unsafe {}` suppresses. |
| 752 | + * |
743 | 753 | * All checks share a single token scan per fn body. The outer loop runs once, |
744 | 754 | * skipping nested fn bodies once. Per-token dispatch is a switch on tok.text |
745 | 755 | * after a kind==="ident" guard. |
@@ -1027,6 +1037,7 @@ export function passSynCheck(src: string, version: VersionInfo): SynCheckResult |
1027 | 1037 | const syn067 = getErrorCode("SYN067")!; |
1028 | 1038 | const syn068 = getErrorCode("SYN068")!; |
1029 | 1039 | const syn069 = getErrorCode("SYN069")!; |
| 1040 | + const syn070 = getErrorCode("SYN070")!; |
1030 | 1041 |
|
1031 | 1042 | // Collect char-offset ranges where all SYN checks are suppressed: |
1032 | 1043 | // 1. `unsafe "reason" { ... }` expression blocks — explicit acknowledgment. |
@@ -1928,6 +1939,123 @@ export function passSynCheck(src: string, version: VersionInfo): SynCheckResult |
1928 | 1939 | }); |
1929 | 1940 | } |
1930 | 1941 |
|
| 1942 | + // ── SYN070: inline array-element .at(N) bypass — pre-pass ─────────────── |
| 1943 | + // Pattern: [guarded_global].at(N)(...) — guarded global at index N in an |
| 1944 | + // inline array literal, retrieved via Array.prototype.at(N) and called. |
| 1945 | + // Mirrors SYN069 but for the .at() method form instead of [N] bracket form. |
| 1946 | + for (let i70 = bodyStart; i70 < decl.tokenEnd; i70++) { |
| 1947 | + const tok70 = tokens[i70]; |
| 1948 | + if (!tok70 || tok70.kind !== "ident") continue; |
| 1949 | + if (!SYN037_GUARDED_GLOBALS.has(tok70.text)) continue; |
| 1950 | + |
| 1951 | + // Must be in array-element position: prev significant token is `[` or `,` |
| 1952 | + const prevIdx70 = prevSignificant(tokens, i70 - 1); |
| 1953 | + const prev70 = tokens[prevIdx70]; |
| 1954 | + const inArray70 = |
| 1955 | + (prev70 && prev70.kind === "open" && prev70.text === "[") || |
| 1956 | + (prev70 && prev70.kind === "punct" && prev70.text === ","); |
| 1957 | + if (!inArray70) continue; |
| 1958 | + |
| 1959 | + // Backward scan to find the opening `[` of the enclosing array literal. |
| 1960 | + let openBracketIdx70 = -1; |
| 1961 | + { |
| 1962 | + let d70 = 0; |
| 1963 | + for (let j = i70 - 1; j >= bodyStart; j--) { |
| 1964 | + const t = tokens[j]; |
| 1965 | + if (!t) continue; |
| 1966 | + if (t.kind === "close") { d70++; continue; } |
| 1967 | + if (t.kind === "open") { |
| 1968 | + if (d70 === 0 && t.text === "[") { openBracketIdx70 = j; break; } |
| 1969 | + d70--; |
| 1970 | + } |
| 1971 | + } |
| 1972 | + } |
| 1973 | + if (openBracketIdx70 < 0) continue; |
| 1974 | + |
| 1975 | + // Count commas at depth 0 between opening `[` and tok70 to determine index. |
| 1976 | + let elemIndex70 = 0; |
| 1977 | + { |
| 1978 | + let d70 = 0; |
| 1979 | + for (let j = openBracketIdx70 + 1; j < i70; j++) { |
| 1980 | + const t = tokens[j]; |
| 1981 | + if (!t) continue; |
| 1982 | + if (t.kind === "open") { d70++; continue; } |
| 1983 | + if (t.kind === "close") { d70--; continue; } |
| 1984 | + if (d70 === 0 && t.kind === "punct" && t.text === ",") elemIndex70++; |
| 1985 | + } |
| 1986 | + } |
| 1987 | + |
| 1988 | + // Use matchedAt to find the closing `]` of the array literal. |
| 1989 | + const closeBracketIdx70 = tokens[openBracketIdx70]!.matchedAt; |
| 1990 | + if (closeBracketIdx70 === undefined) continue; |
| 1991 | + |
| 1992 | + // After `]`, skip any closing parens (handles `([eval]).at(0)(code)`). |
| 1993 | + let afterCloseIdx70 = nextSignificant(tokens, closeBracketIdx70 + 1); |
| 1994 | + while (tokens[afterCloseIdx70]?.kind === "close" && tokens[afterCloseIdx70]?.text === ")") { |
| 1995 | + afterCloseIdx70 = nextSignificant(tokens, afterCloseIdx70 + 1); |
| 1996 | + } |
| 1997 | + |
| 1998 | + // Expect `.at` — a dot token followed by ident `at`. |
| 1999 | + const dotTok70 = tokens[afterCloseIdx70]; |
| 2000 | + if (!dotTok70 || dotTok70.kind !== "punct" || dotTok70.text !== ".") continue; |
| 2001 | + const atIdx70 = nextSignificant(tokens, afterCloseIdx70 + 1); |
| 2002 | + const atTok70 = tokens[atIdx70]; |
| 2003 | + if (!atTok70 || atTok70.kind !== "ident" || atTok70.text !== "at") continue; |
| 2004 | + |
| 2005 | + // Expect `(` opening the .at() argument list. |
| 2006 | + const openParenIdx70 = nextSignificant(tokens, atIdx70 + 1); |
| 2007 | + const openParen70 = tokens[openParenIdx70]; |
| 2008 | + if (!openParen70 || !(openParen70.kind === "open" && openParen70.text === "(")) continue; |
| 2009 | + |
| 2010 | + // Inside the parens, must be a numeric literal. |
| 2011 | + const numIdx70 = nextSignificant(tokens, openParenIdx70 + 1); |
| 2012 | + const numTok70 = tokens[numIdx70]; |
| 2013 | + if (!numTok70 || numTok70.kind !== "number") continue; |
| 2014 | + |
| 2015 | + // The numeric value must match the element index (negative indices not tracked). |
| 2016 | + const indexVal70 = parseInt(numTok70.text, 10); |
| 2017 | + if (isNaN(indexVal70) || indexVal70 < 0 || indexVal70 !== elemIndex70) continue; |
| 2018 | + |
| 2019 | + // After the number, must be closing `)`. |
| 2020 | + const closeParenIdx70 = nextSignificant(tokens, numIdx70 + 1); |
| 2021 | + const closeParen70 = tokens[closeParenIdx70]; |
| 2022 | + if (!closeParen70 || !(closeParen70.kind === "close" && closeParen70.text === ")")) continue; |
| 2023 | + |
| 2024 | + // After `)`, must be `(` or `?.(` — a call on the returned value. |
| 2025 | + const callIdx70 = nextSignificant(tokens, closeParenIdx70 + 1); |
| 2026 | + const callTok70 = tokens[callIdx70]; |
| 2027 | + const isCall70 = |
| 2028 | + callTok70 && ( |
| 2029 | + (callTok70.kind === "open" && callTok70.text === "(") || |
| 2030 | + callTok70.kind === "questionDot" |
| 2031 | + ); |
| 2032 | + if (!isCall70) continue; |
| 2033 | + |
| 2034 | + if (isInsideRange(tok70.start, unsafeRanges)) continue; |
| 2035 | + |
| 2036 | + const loc70 = locationOf(src, tok70.start); |
| 2037 | + warnings.push({ |
| 2038 | + code: "SYN070", |
| 2039 | + severity: "warning", |
| 2040 | + file: null, |
| 2041 | + line: loc70.line, |
| 2042 | + column: loc70.column, |
| 2043 | + start: tok70.start, |
| 2044 | + end: closeParen70!.end, |
| 2045 | + message: |
| 2046 | + `fn '${decl.name}' stores ${tok70.text} at index ${elemIndex70} of an inline array ` + |
| 2047 | + `then calls it via [${tok70.text}].at(${elemIndex70})(...) — ` + |
| 2048 | + `SYN004/SYN007/… only fire when ${tok70.text} is in call position (followed by '('); ` + |
| 2049 | + `SYN069 closes the [N] bracket form but .at(N) is the modern equivalent and bypasses it; ` + |
| 2050 | + `alias-binding checks (SYN044–SYN068) only track binding declarations, not inline array elements; ` + |
| 2051 | + `the runtime effect is identical to calling ${tok70.text}(...) directly; ` + |
| 2052 | + `refactor to call ${tok70.text} directly or wrap in unsafe "reason" { [${tok70.text}].at(${elemIndex70})(...) }`, |
| 2053 | + rule: syn070.rule, |
| 2054 | + idiom: syn070.idiom, |
| 2055 | + rewrite: syn070.rewrite, |
| 2056 | + }); |
| 2057 | + } |
| 2058 | + |
1931 | 2059 | // Single dispatch loop: nesting bookkeeping runs once per token position. |
1932 | 2060 | // All SYN checks are dispatched via a switch on tok.text after an ident guard. |
1933 | 2061 | for (let i = bodyStart; i < decl.tokenEnd; i++) { |
|
0 commit comments