@colixsystems/widget-sdk 0.275.1 → 0.275.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/linter.cjs +444 -162
- package/dist/linter.js +444 -164
- package/package.json +1 -1
package/dist/linter.cjs
CHANGED
|
@@ -528,15 +528,22 @@ const _ASSET_SPECIFIER_RE = /\.(css|png|jpe?g|gif)$/i;
|
|
|
528
528
|
const _ICON_DEFAULT_CONFIGURED_RE =
|
|
529
529
|
/\bIcon\s*\.\s*Default\s*(\.\s*mergeOptions\s*\(|\.\s*imagePath\s*=)/;
|
|
530
530
|
|
|
531
|
-
//
|
|
531
|
+
// 1-based line of any index in `code`, by binary search over the newline
|
|
532
|
+
// offsets: counting from the top per finding is quadratic (sc-8204).
|
|
532
533
|
function _lineCounter(code) {
|
|
533
|
-
|
|
534
|
-
let
|
|
534
|
+
const newlines = [];
|
|
535
|
+
for (let i = code.indexOf("\n"); i !== -1; i = code.indexOf("\n", i + 1)) {
|
|
536
|
+
newlines.push(i);
|
|
537
|
+
}
|
|
535
538
|
return (index) => {
|
|
536
|
-
|
|
537
|
-
|
|
539
|
+
let lo = 0;
|
|
540
|
+
let hi = newlines.length;
|
|
541
|
+
while (lo < hi) {
|
|
542
|
+
const mid = (lo + hi) >> 1;
|
|
543
|
+
if (newlines[mid] < index) lo = mid + 1;
|
|
544
|
+
else hi = mid;
|
|
538
545
|
}
|
|
539
|
-
return
|
|
546
|
+
return lo + 1;
|
|
540
547
|
};
|
|
541
548
|
}
|
|
542
549
|
|
|
@@ -584,6 +591,9 @@ function _leafletRules(source) {
|
|
|
584
591
|
let tag;
|
|
585
592
|
while ((tag = _MARKER_TAG_RE.exec(code))) {
|
|
586
593
|
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
594
|
+
// Resume past the tag even when it passes: rescanning a tag nested in
|
|
595
|
+
// its attributes made unclosed runs quadratic (sc-8204).
|
|
596
|
+
_MARKER_TAG_RE.lastIndex = end;
|
|
587
597
|
if (/(^|\s)icon\s*=/.test(code.slice(tag.index, end))) continue;
|
|
588
598
|
const line = markerLineAt(tag.index);
|
|
589
599
|
findings.push({
|
|
@@ -601,7 +611,6 @@ function _leafletRules(source) {
|
|
|
601
611
|
line,
|
|
602
612
|
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
603
613
|
});
|
|
604
|
-
_MARKER_TAG_RE.lastIndex = end;
|
|
605
614
|
}
|
|
606
615
|
return findings;
|
|
607
616
|
}
|
|
@@ -1043,8 +1052,32 @@ const LUCIDE_NAME_SET = new Set(LUCIDE_ICON_NAMES);
|
|
|
1043
1052
|
// `[^}]*` (not `[\s\S]*?`) so the brace capture can never cross a `}` into a
|
|
1044
1053
|
// neighbouring import — otherwise a preceding `import { View, Pressable } from
|
|
1045
1054
|
// "react-native"` is swallowed and its names get validated as lucide icons.
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1055
|
+
const LUCIDE_IMPORT_HEAD_RE = /import\s+(?:[A-Za-z0-9_$]+\s*,\s*)?\{/g;
|
|
1056
|
+
const LUCIDE_IMPORT_TAIL_RE = /\}\s*from\s*["']lucide-react-native["']/y;
|
|
1057
|
+
|
|
1058
|
+
// `import { … } from "lucide-react-native"` clauses. The names run to the
|
|
1059
|
+
// NEXT `}`, so every head sharing that brace shares one tail check; one
|
|
1060
|
+
// regex retried that run from each unclosed `import {` (sc-8204).
|
|
1061
|
+
function _lucideImports(source) {
|
|
1062
|
+
const found = [];
|
|
1063
|
+
const tails = new Map();
|
|
1064
|
+
LUCIDE_IMPORT_HEAD_RE.lastIndex = 0;
|
|
1065
|
+
let head;
|
|
1066
|
+
while ((head = LUCIDE_IMPORT_HEAD_RE.exec(source))) {
|
|
1067
|
+
const from = LUCIDE_IMPORT_HEAD_RE.lastIndex;
|
|
1068
|
+
const close = source.indexOf("}", from);
|
|
1069
|
+
if (close === -1) break;
|
|
1070
|
+
if (!tails.has(close)) {
|
|
1071
|
+
LUCIDE_IMPORT_TAIL_RE.lastIndex = close;
|
|
1072
|
+
tails.set(close, LUCIDE_IMPORT_TAIL_RE.test(source) ? LUCIDE_IMPORT_TAIL_RE.lastIndex : -1);
|
|
1073
|
+
}
|
|
1074
|
+
const end = tails.get(close);
|
|
1075
|
+
if (end === -1) continue;
|
|
1076
|
+
found.push({ index: head.index, names: source.slice(from, close) });
|
|
1077
|
+
LUCIDE_IMPORT_HEAD_RE.lastIndex = end;
|
|
1078
|
+
}
|
|
1079
|
+
return found;
|
|
1080
|
+
}
|
|
1048
1081
|
|
|
1049
1082
|
// lucide re-exports each base icon as `<Name>`, `<Name>Icon`, and `Lucide<Name>`;
|
|
1050
1083
|
// the committed set holds only the base names, so normalise the alias forms
|
|
@@ -1060,11 +1093,10 @@ function _isValidLucideName(name) {
|
|
|
1060
1093
|
function _lucideIconRules(source) {
|
|
1061
1094
|
const findings = [];
|
|
1062
1095
|
const sourceLines = source.split(/\r?\n/);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
for (const raw of (m[1] || "").split(",")) {
|
|
1096
|
+
const lineAt = _lineCounter(source);
|
|
1097
|
+
for (const m of _lucideImports(source)) {
|
|
1098
|
+
const line = lineAt(m.index);
|
|
1099
|
+
for (const raw of m.names.split(",")) {
|
|
1068
1100
|
// Strip any inline comment from the specifier token so a `{ Home, // x`
|
|
1069
1101
|
// comment can't glue onto and hide the next specifier (e.g. `House`).
|
|
1070
1102
|
const cleaned = raw
|
|
@@ -1147,6 +1179,25 @@ const _EXPRESSION_LEAD_WORDS = new Set([
|
|
|
1147
1179
|
"instanceof",
|
|
1148
1180
|
]);
|
|
1149
1181
|
|
|
1182
|
+
// The offset just past the whitespace run ending before `at`.
|
|
1183
|
+
function _skipSpaceBack(code, at) {
|
|
1184
|
+
while (at > 0 && /\s/.test(code[at - 1])) at--;
|
|
1185
|
+
return at;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function _skipSpaceForward(code, at) {
|
|
1189
|
+
while (at < code.length && /\s/.test(code[at])) at++;
|
|
1190
|
+
return at;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// The identifier ending at `last`; leading digits are not part of it.
|
|
1194
|
+
function _wordEndingAt(code, last) {
|
|
1195
|
+
let start = last + 1;
|
|
1196
|
+
while (start > 0 && /[A-Za-z0-9_$]/.test(code[start - 1])) start--;
|
|
1197
|
+
while (start <= last && /[0-9]/.test(code[start])) start++;
|
|
1198
|
+
return code.slice(start, last + 1);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1150
1201
|
// 1-based line of the first unbound reference to `name`, or 0 when the name is
|
|
1151
1202
|
// bound, never referenced, or referenced in a form this scan cannot decide.
|
|
1152
1203
|
function _unboundReferenceLine(code, name) {
|
|
@@ -1156,22 +1207,24 @@ function _unboundReferenceLine(code, name) {
|
|
|
1156
1207
|
const word = new RegExp(`\\b${name}\\b`, "g");
|
|
1157
1208
|
let firstIndex = -1;
|
|
1158
1209
|
let match;
|
|
1210
|
+
// Every look-around walks a few characters from the match: slicing and
|
|
1211
|
+
// regex-testing the whole prefix per match was quadratic (sc-8204).
|
|
1159
1212
|
while ((match = word.exec(code))) {
|
|
1160
|
-
const
|
|
1213
|
+
const leadAt = _skipSpaceBack(code, match.index) - 1;
|
|
1214
|
+
const lead = leadAt >= 0 ? code[leadAt] : "";
|
|
1161
1215
|
// `obj.NAME` / `obj?.NAME` / `{...NAME}` — never this binding.
|
|
1162
|
-
if (
|
|
1163
|
-
const lead = before.slice(-1);
|
|
1216
|
+
if (lead === ".") continue;
|
|
1164
1217
|
if (/[A-Za-z0-9_$]/.test(lead)) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
} else if (lead === ">" && !before.endsWith("=>")) {
|
|
1218
|
+
if (!_EXPRESSION_LEAD_WORDS.has(_wordEndingAt(code, leadAt))) return 0;
|
|
1219
|
+
} else if (lead === ">" && code[leadAt - 1] !== "=") {
|
|
1168
1220
|
// A `>` that is not an arrow closes a JSX tag, so what follows is text.
|
|
1169
1221
|
return 0;
|
|
1170
1222
|
}
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1223
|
+
const opensTag =
|
|
1224
|
+
lead === "<" ||
|
|
1225
|
+
(lead === "/" && code[_skipSpaceBack(code, leadAt) - 1] === "<");
|
|
1226
|
+
const next = code[_skipSpaceForward(code, match.index + name.length)];
|
|
1227
|
+
if (!opensTag && next !== "(" && next !== ".") return 0;
|
|
1175
1228
|
if (firstIndex < 0) firstIndex = match.index;
|
|
1176
1229
|
}
|
|
1177
1230
|
if (firstIndex < 0) return 0;
|
|
@@ -1253,10 +1306,11 @@ function _bundledAssetRules(source, assetFiles) {
|
|
|
1253
1306
|
// Comments blanked, strings kept: the file name IS a string literal.
|
|
1254
1307
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
1255
1308
|
const sourceLines = source.split(/\r?\n/);
|
|
1309
|
+
const lineAt = _lineCounter(code);
|
|
1256
1310
|
for (const match of code.matchAll(BUNDLED_ASSET_CALL_RE)) {
|
|
1257
1311
|
const name = match[1] || match[2] || match[3] || "";
|
|
1258
1312
|
if (name.includes("${") || assetFiles.includes(name)) continue;
|
|
1259
|
-
const line =
|
|
1313
|
+
const line = lineAt(match.index);
|
|
1260
1314
|
findings.push({
|
|
1261
1315
|
rule: "bundled-asset-missing",
|
|
1262
1316
|
severity: "error",
|
|
@@ -1283,6 +1337,7 @@ function _paymentCurrencyRules(source) {
|
|
|
1283
1337
|
// Comments blanked, strings kept: the currency code IS a string literal.
|
|
1284
1338
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
1285
1339
|
const sourceLines = source.split(/\r?\n/);
|
|
1340
|
+
const lineAt = _lineCounter(code);
|
|
1286
1341
|
let from = 0;
|
|
1287
1342
|
for (;;) {
|
|
1288
1343
|
const at = code.indexOf(REQUEST_PAYMENT_CALL, from);
|
|
@@ -1293,7 +1348,7 @@ function _paymentCurrencyRules(source) {
|
|
|
1293
1348
|
);
|
|
1294
1349
|
if (!match) continue;
|
|
1295
1350
|
// Line of the currency literal itself, not of the call.
|
|
1296
|
-
const line =
|
|
1351
|
+
const line = lineAt(at + match.index);
|
|
1297
1352
|
findings.push({
|
|
1298
1353
|
rule: "payment-currency",
|
|
1299
1354
|
severity: "error",
|
|
@@ -1472,22 +1527,36 @@ function _datastoreErrorHandlingRules(source) {
|
|
|
1472
1527
|
// the deps array is wrong (sc-1579's volatile-query gate exists because of
|
|
1473
1528
|
// exactly this mistake). Flags only when the file has NOT already reached
|
|
1474
1529
|
// for `useStableQuery`, the hook that removes the deps array entirely.
|
|
1530
|
+
// The first `const NAME = useMemo(` of every name, in one pass: a regex
|
|
1531
|
+
// per queried name rescanned the source per call (sc-8204). A name holding
|
|
1532
|
+
// `$` never matched the old per-name pattern, so it is left out.
|
|
1533
|
+
function _firstUseMemoConsts(code) {
|
|
1534
|
+
const first = new Map();
|
|
1535
|
+
const re = /\bconst\s+([A-Za-z_][\w$]*)\s*=\s*useMemo\s*\(/g;
|
|
1536
|
+
let m;
|
|
1537
|
+
while ((m = re.exec(code))) {
|
|
1538
|
+
if (!m[1].includes("$") && !first.has(m[1])) first.set(m[1], m.index);
|
|
1539
|
+
}
|
|
1540
|
+
return first;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1475
1543
|
function _rawUseMemoIntoDatastoreQueryRules(source) {
|
|
1476
1544
|
const code = _stripNonCode(source);
|
|
1477
1545
|
if (/\buseStableQuery\s*\(/.test(code)) return [];
|
|
1478
1546
|
const callRe = /\buseDatastoreQuery\s*\(\s*[^,()]+,\s*([A-Za-z_$][\w$]*)\s*[,)]/g;
|
|
1479
1547
|
const findings = [];
|
|
1480
1548
|
const sourceLines = source.split(/\r?\n/);
|
|
1549
|
+
const lineAt = _lineCounter(code);
|
|
1550
|
+
const memoAt = _firstUseMemoConsts(code);
|
|
1481
1551
|
const seen = new Set();
|
|
1482
1552
|
let m;
|
|
1483
1553
|
while ((m = callRe.exec(code))) {
|
|
1484
1554
|
const varName = m[1];
|
|
1485
1555
|
if (seen.has(varName)) continue;
|
|
1486
|
-
const
|
|
1487
|
-
|
|
1488
|
-
if (!memoMatch || memoMatch.index >= m.index) continue;
|
|
1556
|
+
const memoIndex = memoAt.get(varName);
|
|
1557
|
+
if (memoIndex === undefined || memoIndex >= m.index) continue;
|
|
1489
1558
|
seen.add(varName);
|
|
1490
|
-
const line =
|
|
1559
|
+
const line = lineAt(memoIndex);
|
|
1491
1560
|
findings.push({
|
|
1492
1561
|
rule: "raw-useMemo-into-datastore-query",
|
|
1493
1562
|
severity: "warning",
|
|
@@ -1560,30 +1629,81 @@ function _rowActionGatingRules(source) {
|
|
|
1560
1629
|
// The useDatastoreMutation() call whose update or delete the widget uses,
|
|
1561
1630
|
// either as `m.update(` / `m.delete(` or destructured `{ update, delete }`.
|
|
1562
1631
|
function _rowMutationCall(code) {
|
|
1563
|
-
const
|
|
1564
|
-
for (const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1632
|
+
const rowVerbCallers = new Set();
|
|
1633
|
+
for (const m of code.matchAll(/\b(\w+)\s*\.\s*(?:update|delete)\s*\(/g)) {
|
|
1634
|
+
rowVerbCallers.add(m[1]);
|
|
1635
|
+
}
|
|
1636
|
+
for (const call of _mutationHookCalls(code)) {
|
|
1637
|
+
// An empty `{}` read as a name before: the regex built "undefined".
|
|
1638
|
+
const usesRowVerb = call.destructured
|
|
1639
|
+
? /\b(update|delete)\b/.test(call.destructured)
|
|
1640
|
+
: rowVerbCallers.has(String(call.name));
|
|
1641
|
+
if (usesRowVerb) return call;
|
|
1570
1642
|
}
|
|
1571
1643
|
return null;
|
|
1572
1644
|
}
|
|
1573
1645
|
|
|
1646
|
+
const _MUTATION_TAIL_RE = /\}\s*=\s*useDatastoreMutation\s*\(/y;
|
|
1647
|
+
const _NAMED_MUTATION_RE = /\b(\w+)\s*=\s*useDatastoreMutation\s*\(/g;
|
|
1648
|
+
|
|
1649
|
+
// Every `{ … } = useDatastoreMutation(` / `name = useDatastoreMutation(`, in
|
|
1650
|
+
// source order, as one `(?:\{([^}]*)\}|\b(\w+))…` regex would match them.
|
|
1651
|
+
// The destructured names run to the NEXT `}`, so each `}` is checked once;
|
|
1652
|
+
// the regex re-read that run from every unclosed `{` (sc-8204).
|
|
1653
|
+
function _mutationHookCalls(code) {
|
|
1654
|
+
const calls = [];
|
|
1655
|
+
const tails = new Map();
|
|
1656
|
+
let named = null;
|
|
1657
|
+
let brace = code.indexOf("{");
|
|
1658
|
+
let close = -1;
|
|
1659
|
+
for (let from = 0; ; ) {
|
|
1660
|
+
if (named === null || named.index < from) {
|
|
1661
|
+
_NAMED_MUTATION_RE.lastIndex = from;
|
|
1662
|
+
named = _NAMED_MUTATION_RE.exec(code) || { index: Infinity };
|
|
1663
|
+
}
|
|
1664
|
+
let destructured = null;
|
|
1665
|
+
for (; brace !== -1 && brace < named.index; brace = code.indexOf("{", brace + 1)) {
|
|
1666
|
+
if (brace < from) continue;
|
|
1667
|
+
if (close <= brace) close = code.indexOf("}", brace + 1);
|
|
1668
|
+
if (close === -1) {
|
|
1669
|
+
brace = -1;
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
if (!tails.has(close)) {
|
|
1673
|
+
_MUTATION_TAIL_RE.lastIndex = close;
|
|
1674
|
+
tails.set(close, _MUTATION_TAIL_RE.test(code) ? _MUTATION_TAIL_RE.lastIndex : -1);
|
|
1675
|
+
}
|
|
1676
|
+
if (tails.get(close) !== -1) {
|
|
1677
|
+
destructured = { index: brace, destructured: code.slice(brace + 1, close) };
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (destructured) {
|
|
1682
|
+
calls.push(destructured);
|
|
1683
|
+
from = tails.get(close);
|
|
1684
|
+
} else if (named.index !== Infinity) {
|
|
1685
|
+
calls.push({ index: named.index, name: named[1] });
|
|
1686
|
+
from = named.index + named[0].length;
|
|
1687
|
+
} else return calls;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1574
1691
|
function _imagePercentHeightRules(source) {
|
|
1575
1692
|
const findings = [];
|
|
1576
1693
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
1577
1694
|
const sourceLines = source.split(/\r?\n/);
|
|
1695
|
+
const lineAt = _lineCounter(code);
|
|
1578
1696
|
_IMAGE_TAG_RE.lastIndex = 0;
|
|
1579
1697
|
let tag;
|
|
1580
1698
|
while ((tag = _IMAGE_TAG_RE.exec(code))) {
|
|
1581
1699
|
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
1700
|
+
// A tag nested in these attributes is inside `attrs` already (sc-8204).
|
|
1701
|
+
_IMAGE_TAG_RE.lastIndex = end;
|
|
1582
1702
|
const attrs = code.slice(tag.index, end);
|
|
1583
1703
|
_PERCENT_HEIGHT_RE.lastIndex = 0;
|
|
1584
1704
|
const hit = _PERCENT_HEIGHT_RE.exec(attrs);
|
|
1585
1705
|
if (!hit) continue;
|
|
1586
|
-
const line =
|
|
1706
|
+
const line = lineAt(tag.index + hit.index);
|
|
1587
1707
|
findings.push({
|
|
1588
1708
|
rule: "image-percent-height",
|
|
1589
1709
|
severity: "warning",
|
|
@@ -1598,7 +1718,6 @@ function _imagePercentHeightRules(source) {
|
|
|
1598
1718
|
line,
|
|
1599
1719
|
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
1600
1720
|
});
|
|
1601
|
-
_IMAGE_TAG_RE.lastIndex = end;
|
|
1602
1721
|
}
|
|
1603
1722
|
return findings;
|
|
1604
1723
|
}
|
|
@@ -1615,10 +1734,11 @@ function _flexBasisPercentRules(source) {
|
|
|
1615
1734
|
const findings = [];
|
|
1616
1735
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
1617
1736
|
const sourceLines = source.split(/\r?\n/);
|
|
1737
|
+
const lineAt = _lineCounter(code);
|
|
1618
1738
|
_FLEX_BASIS_PERCENT_RE.lastIndex = 0;
|
|
1619
1739
|
let hit;
|
|
1620
1740
|
while ((hit = _FLEX_BASIS_PERCENT_RE.exec(code))) {
|
|
1621
|
-
const line =
|
|
1741
|
+
const line = lineAt(hit.index);
|
|
1622
1742
|
findings.push({
|
|
1623
1743
|
rule: "flex-basis-percent",
|
|
1624
1744
|
severity: "warning",
|
|
@@ -1651,42 +1771,45 @@ const _SIDE_MARGIN_KEY_RE =
|
|
|
1651
1771
|
const _ZERO_VALUE_RE =
|
|
1652
1772
|
/^\s*(?:(["'`]?)-?0(?:\.0*)?(?:px)?\1\s*(?:[,}]|$)|(?:undefined|null)\b)/m;
|
|
1653
1773
|
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1774
|
+
|
|
1775
|
+
// The object's own keys only: a nested object's margin is not this box's.
|
|
1776
|
+
// Blanked rather than cut, so an offset here is the same offset in the source.
|
|
1777
|
+
// Partner offset of every brace matched by stack in `text`, else -1.
|
|
1778
|
+
function _bracePartners(text) {
|
|
1779
|
+
const partner = new Int32Array(text.length).fill(-1);
|
|
1780
|
+
const opens = [];
|
|
1781
|
+
for (let i = 0; i < text.length; i++) {
|
|
1782
|
+
if (text[i] === "{") opens.push(i);
|
|
1783
|
+
else if (text[i] === "}" && opens.length > 0) {
|
|
1784
|
+
const open = opens.pop();
|
|
1785
|
+
partner[open] = i;
|
|
1786
|
+
partner[i] = open;
|
|
1662
1787
|
}
|
|
1663
1788
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1789
|
+
return partner;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// An object's own level: each nested `{…}` collapsed to one space, with the
|
|
1793
|
+
// offset in `text` of every character kept. Walking only that level means
|
|
1794
|
+
// an ancestor never re-reads its descendants (sc-8204).
|
|
1795
|
+
function _ownKeys(text, partner, [start, end]) {
|
|
1796
|
+
let own = "";
|
|
1797
|
+
const at = [];
|
|
1798
|
+
for (let i = start; i < end; i++) {
|
|
1799
|
+
const close = partner[i];
|
|
1800
|
+
const nested = text[i] === "{" && i !== start && close > i && close < end - 1;
|
|
1801
|
+
own += nested ? " " : text[i];
|
|
1802
|
+
at.push(i);
|
|
1803
|
+
if (nested) i = close;
|
|
1668
1804
|
}
|
|
1669
|
-
return
|
|
1805
|
+
return { own, at };
|
|
1670
1806
|
}
|
|
1671
1807
|
|
|
1672
|
-
|
|
1673
|
-
// Blanked rather than cut, so an offset here is the same offset in the source.
|
|
1674
|
-
function _ownKeysMask(structure, [start, end]) {
|
|
1675
|
-
let body = structure.slice(start, end);
|
|
1676
|
-
let previous;
|
|
1677
|
-
do {
|
|
1678
|
-
previous = body;
|
|
1679
|
-
body = body.replace(/(?<!^)\{[^{}]*\}(?!$)/g, (nested) => " ".repeat(nested.length));
|
|
1680
|
-
} while (body !== previous);
|
|
1681
|
-
return body;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
function _hasSideMargin(code, structure, span) {
|
|
1685
|
-
const keys = _ownKeysMask(structure, span);
|
|
1808
|
+
function _hasSideMargin(code, keys) {
|
|
1686
1809
|
_SIDE_MARGIN_KEY_RE.lastIndex = 0;
|
|
1687
1810
|
let key;
|
|
1688
|
-
while ((key = _SIDE_MARGIN_KEY_RE.exec(keys))) {
|
|
1689
|
-
const valueAt =
|
|
1811
|
+
while ((key = _SIDE_MARGIN_KEY_RE.exec(keys.own))) {
|
|
1812
|
+
const valueAt = keys.at[key.index + key[0].length - 1] + 1;
|
|
1690
1813
|
if (!_ZERO_VALUE_RE.test(code.slice(valueAt, valueAt + 40))) return true;
|
|
1691
1814
|
}
|
|
1692
1815
|
return false;
|
|
@@ -1697,14 +1820,21 @@ function _fullWidthMarginRules(source) {
|
|
|
1697
1820
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
1698
1821
|
// Strings blanked, so a brace inside a literal never unbalances the match.
|
|
1699
1822
|
const structure = _stripNonCode(source);
|
|
1823
|
+
const index = _jsxScanIndex(structure);
|
|
1824
|
+
const partner = _bracePartners(structure);
|
|
1825
|
+
const margined = new Map();
|
|
1700
1826
|
const sourceLines = source.split(/\r?\n/);
|
|
1827
|
+
const lineAt = _lineCounter(code);
|
|
1701
1828
|
_FULL_WIDTH_RE.lastIndex = 0;
|
|
1702
1829
|
let hit;
|
|
1703
1830
|
while ((hit = _FULL_WIDTH_RE.exec(code))) {
|
|
1704
|
-
const span =
|
|
1831
|
+
const span = index.enclosingObject(hit.index);
|
|
1705
1832
|
if (!span) continue;
|
|
1706
|
-
if (!
|
|
1707
|
-
|
|
1833
|
+
if (!margined.has(span[0])) {
|
|
1834
|
+
margined.set(span[0], _hasSideMargin(code, _ownKeys(structure, partner, span)));
|
|
1835
|
+
}
|
|
1836
|
+
if (!margined.get(span[0])) continue;
|
|
1837
|
+
const line = lineAt(hit.index);
|
|
1708
1838
|
findings.push({
|
|
1709
1839
|
rule: "full-width-margin-overflow",
|
|
1710
1840
|
severity: "warning",
|
|
@@ -1743,53 +1873,98 @@ function _styleEntryOffsets(structure) {
|
|
|
1743
1873
|
return offsets;
|
|
1744
1874
|
}
|
|
1745
1875
|
|
|
1746
|
-
//
|
|
1747
|
-
|
|
1876
|
+
// What a style expression's objects say, ORed: the object literals it
|
|
1877
|
+
// spans, plus the named entries it references anywhere inside.
|
|
1878
|
+
function _styleRangeTraits(scan, [from, to]) {
|
|
1748
1879
|
const { structure } = scan;
|
|
1749
|
-
|
|
1880
|
+
let row = false;
|
|
1881
|
+
let bordered = false;
|
|
1750
1882
|
for (let i = from + 1; i < to - 1; i++) {
|
|
1751
1883
|
if (structure[i] !== "{") continue;
|
|
1752
|
-
const span =
|
|
1884
|
+
const span = scan.index.enclosingObject(i + 1);
|
|
1753
1885
|
if (!span) continue;
|
|
1754
|
-
|
|
1886
|
+
const traits = _styleObjectTraits(scan, span);
|
|
1887
|
+
row = row || traits.row;
|
|
1888
|
+
bordered = bordered || traits.bordered;
|
|
1755
1889
|
i = span[1] - 1;
|
|
1756
1890
|
}
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1891
|
+
const refs = _styleRefTraits(scan);
|
|
1892
|
+
const first = _firstAtOrAfter(refs.offsets, from);
|
|
1893
|
+
const last = _firstAtOrAfter(refs.offsets, to);
|
|
1894
|
+
return {
|
|
1895
|
+
row: row || refs.rows[last] > refs.rows[first],
|
|
1896
|
+
bordered: bordered || refs.bordered[last] > refs.bordered[first],
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Every `name.key` reference in the source, with running counts of those
|
|
1901
|
+
// naming a row / bordered entry, so a range is answered by subtraction
|
|
1902
|
+
// instead of re-reading it per nested parent (sc-8204).
|
|
1903
|
+
function _styleRefTraits(scan) {
|
|
1904
|
+
if (scan.refs) return scan.refs;
|
|
1905
|
+
if (!scan.entries) scan.entries = _styleEntryOffsets(scan.structure);
|
|
1906
|
+
const offsets = [];
|
|
1907
|
+
const rows = [0];
|
|
1908
|
+
const bordered = [0];
|
|
1759
1909
|
_STYLE_REF_RE.lastIndex = 0;
|
|
1760
|
-
const refs = structure.slice(from, to);
|
|
1761
1910
|
let ref;
|
|
1762
|
-
while ((ref = _STYLE_REF_RE.exec(
|
|
1763
|
-
|
|
1764
|
-
const
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1911
|
+
while ((ref = _STYLE_REF_RE.exec(scan.structure))) {
|
|
1912
|
+
const at = scan.entries.get(ref[1]);
|
|
1913
|
+
const span = at === undefined ? null : scan.index.enclosingObject(at);
|
|
1914
|
+
const traits = span ? _styleObjectTraits(scan, span) : null;
|
|
1915
|
+
offsets.push(ref.index);
|
|
1916
|
+
rows.push(rows[rows.length - 1] + (traits && traits.row ? 1 : 0));
|
|
1917
|
+
bordered.push(bordered[bordered.length - 1] + (traits && traits.bordered ? 1 : 0));
|
|
1918
|
+
}
|
|
1919
|
+
scan.refs = { offsets, rows, bordered };
|
|
1920
|
+
return scan.refs;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function _firstAtOrAfter(sorted, value) {
|
|
1924
|
+
let lo = 0;
|
|
1925
|
+
let hi = sorted.length;
|
|
1926
|
+
while (lo < hi) {
|
|
1927
|
+
const mid = (lo + hi) >> 1;
|
|
1928
|
+
if (sorted[mid] < value) lo = mid + 1;
|
|
1929
|
+
else hi = mid;
|
|
1767
1930
|
}
|
|
1768
|
-
return
|
|
1931
|
+
return lo;
|
|
1769
1932
|
}
|
|
1770
1933
|
|
|
1771
1934
|
function _isBorderedRow(scan, tag) {
|
|
1772
|
-
const {
|
|
1935
|
+
const { structure } = scan;
|
|
1773
1936
|
const attrs = structure.slice(tag.attrs[0], tag.attrs[1]);
|
|
1774
1937
|
const style = _STYLE_ATTR_RE.exec(attrs);
|
|
1775
1938
|
if (!style) return false;
|
|
1776
1939
|
const open = tag.attrs[0] + style.index + style[0].length - 1;
|
|
1777
|
-
const
|
|
1778
|
-
if (
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1940
|
+
const close = scan.index.braceClose(open);
|
|
1941
|
+
if (close === -1) return false;
|
|
1942
|
+
const traits = _styleRangeTraits(scan, [open, close + 1]);
|
|
1943
|
+
return traits.row && traits.bordered;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Memoised per object: every row naming one shared style object would
|
|
1947
|
+
// otherwise re-read all of it, quadratic in the row count (sc-8204).
|
|
1948
|
+
function _styleObjectTraits(scan, span) {
|
|
1949
|
+
const known = scan.traits.get(span[0]);
|
|
1950
|
+
if (known) return known;
|
|
1951
|
+
const { code, structure } = scan;
|
|
1952
|
+
if (!scan.partners) {
|
|
1953
|
+
scan.partners = { code: _bracePartners(code), structure: _bracePartners(structure) };
|
|
1954
|
+
}
|
|
1955
|
+
const keys = _ownKeys(structure, scan.partners.structure, span);
|
|
1956
|
+
const traits = {
|
|
1957
|
+
row: _ROW_KEY_RE.test(_ownKeys(code, scan.partners.code, span).own),
|
|
1958
|
+
bordered: false,
|
|
1959
|
+
};
|
|
1960
|
+
_BORDER_WIDTH_KEY_RE.lastIndex = 0;
|
|
1961
|
+
let key;
|
|
1962
|
+
while ((key = _BORDER_WIDTH_KEY_RE.exec(keys.own))) {
|
|
1963
|
+
const valueAt = keys.at[key.index + key[0].length - 1] + 1;
|
|
1964
|
+
if (!_ZERO_VALUE_RE.test(code.slice(valueAt, valueAt + 40))) traits.bordered = true;
|
|
1791
1965
|
}
|
|
1792
|
-
|
|
1966
|
+
scan.traits.set(span[0], traits);
|
|
1967
|
+
return traits;
|
|
1793
1968
|
}
|
|
1794
1969
|
|
|
1795
1970
|
function _selfClosing(structure, end) {
|
|
@@ -1798,13 +1973,46 @@ function _selfClosing(structure, end) {
|
|
|
1798
1973
|
return structure[i] === "/";
|
|
1799
1974
|
}
|
|
1800
1975
|
|
|
1976
|
+
// The open-element stack, with where each name sits on it, so a closing
|
|
1977
|
+
// tag finds its opener without scanning the stack per close (sc-8204).
|
|
1978
|
+
function _openTagStack() {
|
|
1979
|
+
const tags = [];
|
|
1980
|
+
const byName = new Map();
|
|
1981
|
+
const truncate = (length) => {
|
|
1982
|
+
while (tags.length > length) {
|
|
1983
|
+
const tag = tags.pop();
|
|
1984
|
+
if (tag) byName.get(tag.name).pop();
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
return {
|
|
1988
|
+
top: () => tags[tags.length - 1],
|
|
1989
|
+
push: (tag) => {
|
|
1990
|
+
if (tag) {
|
|
1991
|
+
if (!byName.has(tag.name)) byName.set(tag.name, []);
|
|
1992
|
+
byName.get(tag.name).push(tags.length);
|
|
1993
|
+
}
|
|
1994
|
+
tags.push(tag);
|
|
1995
|
+
},
|
|
1996
|
+
pop: () => truncate(Math.max(0, tags.length - 1)),
|
|
1997
|
+
close: (name) => {
|
|
1998
|
+
const at = byName.get(name);
|
|
1999
|
+
if (at && at.length > 0) truncate(at[at.length - 1]);
|
|
2000
|
+
},
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1801
2004
|
function _fieldInBorderedRowRules(source) {
|
|
1802
2005
|
const findings = [];
|
|
1803
2006
|
const scan = {
|
|
1804
2007
|
code: _stripNonCode(source, { keepStrings: true }),
|
|
1805
2008
|
structure: _stripNonCode(source),
|
|
1806
2009
|
entries: null,
|
|
2010
|
+
index: null,
|
|
2011
|
+
traits: new Map(),
|
|
2012
|
+
partners: null,
|
|
2013
|
+
refs: null,
|
|
1807
2014
|
};
|
|
2015
|
+
scan.index = _jsxScanIndex(scan.structure);
|
|
1808
2016
|
const { code, structure } = scan;
|
|
1809
2017
|
const sourceLines = source.split(/\r?\n/);
|
|
1810
2018
|
// Tokens arrive in source order, so one running count serves every finding.
|
|
@@ -1824,7 +2032,7 @@ function _fieldInBorderedRowRules(source) {
|
|
|
1824
2032
|
snippet: (sourceLines[n - 1] || "").trim().slice(0, 200),
|
|
1825
2033
|
});
|
|
1826
2034
|
};
|
|
1827
|
-
const open =
|
|
2035
|
+
const open = _openTagStack();
|
|
1828
2036
|
_JSX_TOKEN_RE.lastIndex = 0;
|
|
1829
2037
|
let token;
|
|
1830
2038
|
while ((token = _JSX_TOKEN_RE.exec(structure))) {
|
|
@@ -1837,15 +2045,14 @@ function _fieldInBorderedRowRules(source) {
|
|
|
1837
2045
|
continue;
|
|
1838
2046
|
}
|
|
1839
2047
|
if (closing) {
|
|
1840
|
-
|
|
1841
|
-
if (at >= 0) open.length = at;
|
|
2048
|
+
open.close(name);
|
|
1842
2049
|
continue;
|
|
1843
2050
|
}
|
|
1844
|
-
const end =
|
|
2051
|
+
const end = scan.index.tagEnd(token.index);
|
|
1845
2052
|
if (end === -1) break;
|
|
1846
2053
|
const tag = { name, attrs: [token.index + token[0].length, end], borderedRow: undefined };
|
|
1847
2054
|
if (_FIELD_TAG_NAMES.includes(name)) {
|
|
1848
|
-
const parent = open
|
|
2055
|
+
const parent = open.top();
|
|
1849
2056
|
if (parent && parent.borderedRow === undefined) parent.borderedRow = _isBorderedRow(scan, parent);
|
|
1850
2057
|
if (parent && parent.borderedRow) {
|
|
1851
2058
|
report(
|
|
@@ -1885,27 +2092,42 @@ const _WIDTH_HOLDER_RE =
|
|
|
1885
2092
|
const _LAYOUT_FEED_WINDOW = 80;
|
|
1886
2093
|
|
|
1887
2094
|
function _measuredWidthNames(code) {
|
|
2095
|
+
const fedByLayout = _layoutFedCallees(code);
|
|
1888
2096
|
const names = [];
|
|
1889
2097
|
_WIDTH_HOLDER_RE.lastIndex = 0;
|
|
1890
2098
|
let held;
|
|
1891
2099
|
while ((held = _WIDTH_HOLDER_RE.exec(code))) {
|
|
1892
2100
|
const [, value, setter, hook] = held;
|
|
1893
|
-
if (hook === "useContainerWidth") {
|
|
2101
|
+
if (hook === "useContainerWidth" || fedByLayout.has(setter)) {
|
|
1894
2102
|
names.push(value);
|
|
1895
|
-
continue;
|
|
1896
2103
|
}
|
|
1897
|
-
const fedByLayout = new RegExp(
|
|
1898
|
-
`\\b${setter}\\s*\\([\\s\\S]{0,${_LAYOUT_FEED_WINDOW}}?\\blayout\\.width\\b`,
|
|
1899
|
-
);
|
|
1900
|
-
if (fedByLayout.test(code)) names.push(value);
|
|
1901
2104
|
}
|
|
1902
2105
|
return names;
|
|
1903
2106
|
}
|
|
1904
2107
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2108
|
+
// Every callee with a layout width among its first arguments, in one pass:
|
|
2109
|
+
// a regex per declared setter rescanned the source per declaration (sc-8204).
|
|
2110
|
+
function _layoutFedCallees(code) {
|
|
2111
|
+
const fed = new Set();
|
|
2112
|
+
const call = /\b(\w+)\s*\(/g;
|
|
2113
|
+
const feed = new RegExp(
|
|
2114
|
+
`[\\s\\S]{0,${_LAYOUT_FEED_WINDOW}}?\\blayout\\.width\\b`,
|
|
2115
|
+
"y",
|
|
1908
2116
|
);
|
|
2117
|
+
let callee;
|
|
2118
|
+
while ((callee = call.exec(code))) {
|
|
2119
|
+
feed.lastIndex = call.lastIndex;
|
|
2120
|
+
if (feed.test(code)) fed.add(callee[1]);
|
|
2121
|
+
}
|
|
2122
|
+
return fed;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function _sizesFromMeasuredWidth(code, names) {
|
|
2126
|
+
if (names.length === 0) return false;
|
|
2127
|
+
const operands = new Set();
|
|
2128
|
+
for (const m of code.matchAll(/\b(\w+)\s*[-/*]/g)) operands.add(m[1]);
|
|
2129
|
+
for (const m of code.matchAll(/[-/*]\s*\b(\w+)\b/g)) operands.add(m[1]);
|
|
2130
|
+
return names.some((name) => operands.has(name));
|
|
1909
2131
|
}
|
|
1910
2132
|
|
|
1911
2133
|
function _measuredPaddingRules(source) {
|
|
@@ -1913,6 +2135,7 @@ function _measuredPaddingRules(source) {
|
|
|
1913
2135
|
const measured = _measuredWidthNames(code);
|
|
1914
2136
|
if (!_sizesFromMeasuredWidth(code, measured)) return [];
|
|
1915
2137
|
const sourceLines = source.split(/\r?\n/);
|
|
2138
|
+
const lineAt = _lineCounter(code);
|
|
1916
2139
|
const findings = [];
|
|
1917
2140
|
_JSX_TAG_RE.lastIndex = 0;
|
|
1918
2141
|
let tag;
|
|
@@ -1923,9 +2146,7 @@ function _measuredPaddingRules(source) {
|
|
|
1923
2146
|
if (!_ON_LAYOUT_ATTR_RE.test(attrs)) continue;
|
|
1924
2147
|
const padded = _OWN_PADDING_RE.exec(attrs);
|
|
1925
2148
|
if (!padded) continue;
|
|
1926
|
-
const line =
|
|
1927
|
-
.slice(0, tag.index + padded.index)
|
|
1928
|
-
.split(/\r?\n/).length;
|
|
2149
|
+
const line = lineAt(tag.index + padded.index);
|
|
1929
2150
|
findings.push({
|
|
1930
2151
|
rule: "measured-width-ignores-padding",
|
|
1931
2152
|
severity: "warning",
|
|
@@ -2081,6 +2302,17 @@ function _hardcodedDesignRules(source) {
|
|
|
2081
2302
|
* map, an array of sources, or one source string
|
|
2082
2303
|
* @returns {{ ok: boolean, findings: Array<{ rule: string, severity: string, label: string, line: number, snippet: string }> }}
|
|
2083
2304
|
*/
|
|
2305
|
+
// Whether `\b<name>\b` matches `code`, from one pass over its words: a
|
|
2306
|
+
// regex per styleSchema field re-read the whole bundle per field (sc-8204).
|
|
2307
|
+
function _wordReader(code) {
|
|
2308
|
+
const words = new Set(code.match(/\w+/g));
|
|
2309
|
+
return (name) => {
|
|
2310
|
+
if (!name.includes("$")) return words.has(name);
|
|
2311
|
+
// Unescaped, `$` asserts end of input, so only the tail can match.
|
|
2312
|
+
return new RegExp(`\\b${name}\\b`).test(code.slice(-(name.length + 1)));
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2084
2316
|
function lintStyleWiring(manifest, files) {
|
|
2085
2317
|
const empty = { ok: true, findings: [] };
|
|
2086
2318
|
const schema = manifest && manifest.styleSchema;
|
|
@@ -2096,12 +2328,13 @@ function lintStyleWiring(manifest, files) {
|
|
|
2096
2328
|
? _WHOLE_WIDGET_STYLE_FIELDS
|
|
2097
2329
|
: new Set();
|
|
2098
2330
|
const findings = [];
|
|
2331
|
+
const isRead = _wordReader(code);
|
|
2099
2332
|
for (const field of Object.keys(schema)) {
|
|
2100
2333
|
// A key that is not a bare identifier cannot be scanned by name; the
|
|
2101
2334
|
// manifest validator rejects those anyway.
|
|
2102
2335
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(field)) continue;
|
|
2103
2336
|
if (appliedByHelper.has(field)) continue;
|
|
2104
|
-
if (
|
|
2337
|
+
if (isRead(field)) continue;
|
|
2105
2338
|
findings.push({
|
|
2106
2339
|
rule: "style-field-unread",
|
|
2107
2340
|
severity: "warning",
|
|
@@ -2205,49 +2438,92 @@ function _isAuthoredCopy(text) {
|
|
|
2205
2438
|
return !_SEPARATOR_GLYPHS.has(text);
|
|
2206
2439
|
}
|
|
2207
2440
|
|
|
2208
|
-
//
|
|
2209
|
-
//
|
|
2210
|
-
//
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
for (let i =
|
|
2215
|
-
const ch =
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2441
|
+
// Tag ends and brace spans over a STRING-BLANKED mask, where quotes are
|
|
2442
|
+
// already gone, so only brace depth decides. Answered from one index per
|
|
2443
|
+
// source: a scan per match was quadratic, and a file of bare `<Text`
|
|
2444
|
+
// wedged the API pod for a minute (sc-8204).
|
|
2445
|
+
function _jsxScanIndex(mask) {
|
|
2446
|
+
const depth = new Int32Array(mask.length + 1);
|
|
2447
|
+
for (let i = 0; i < mask.length; i++) {
|
|
2448
|
+
const ch = mask[i];
|
|
2449
|
+
depth[i + 1] = depth[i] + (ch === "{" ? 1 : ch === "}" ? -1 : 0);
|
|
2450
|
+
}
|
|
2451
|
+
let tagEnds = null;
|
|
2452
|
+
let braceCloses = null;
|
|
2453
|
+
let braceOpens = null;
|
|
2454
|
+
const closes = () => {
|
|
2455
|
+
if (!braceCloses) braceCloses = _nextAtOrBelow(mask, depth, "}");
|
|
2456
|
+
return braceCloses;
|
|
2457
|
+
};
|
|
2458
|
+
return {
|
|
2459
|
+
// The `>` that CLOSES a JSX tag, skipping any `>` inside a `{…}`
|
|
2460
|
+
// expression — `onPress={() => x}` carries one, and treating it as the
|
|
2461
|
+
// tag end reports the handler body as copy.
|
|
2462
|
+
tagEnd: (from) => {
|
|
2463
|
+
if (!tagEnds) tagEnds = _nextAtOrBelow(mask, depth, ">");
|
|
2464
|
+
return tagEnds[from];
|
|
2465
|
+
},
|
|
2466
|
+
// The `}` balancing the `{` at `from`, or -1.
|
|
2467
|
+
braceClose: (from) => (mask[from] === "{" ? closes()[from + 1] : -1),
|
|
2468
|
+
// The innermost `{…}` around offset `at`, as [start, end), or null.
|
|
2469
|
+
enclosingObject: (at) => {
|
|
2470
|
+
if (!braceOpens) braceOpens = _previousOpenAtOrBelow(mask, depth);
|
|
2471
|
+
const start = braceOpens[at];
|
|
2472
|
+
const end = closes()[at];
|
|
2473
|
+
return start === -1 || end === -1 ? null : [start, end + 1];
|
|
2474
|
+
},
|
|
2475
|
+
};
|
|
2227
2476
|
}
|
|
2228
2477
|
|
|
2229
|
-
//
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2478
|
+
// For every offset i, the first j >= i holding `ch` with depth[j] at or
|
|
2479
|
+
// below depth[i], else -1. One right-to-left pass keeps the candidates on a
|
|
2480
|
+
// stack; a nearer one no deeper dominates, so depth rises toward the top
|
|
2481
|
+
// and each query is a binary search.
|
|
2482
|
+
function _nextAtOrBelow(mask, depth, ch) {
|
|
2483
|
+
const n = mask.length;
|
|
2484
|
+
const nearest = new Int32Array(n + 1).fill(-1);
|
|
2485
|
+
const stack = new Int32Array(n);
|
|
2486
|
+
let top = 0;
|
|
2487
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
2488
|
+
if (mask[i] === ch) {
|
|
2489
|
+
while (top > 0 && depth[stack[top - 1]] >= depth[i]) top--;
|
|
2490
|
+
stack[top++] = i;
|
|
2240
2491
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2492
|
+
nearest[i] = _topmostAtOrBelow(stack, top, depth, 0, depth[i]);
|
|
2493
|
+
}
|
|
2494
|
+
return nearest;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// The mirror for opening braces: for every offset k, the last `{` at i < k
|
|
2498
|
+
// whose inside, depth[i + 1], is at or below depth[k], else -1.
|
|
2499
|
+
function _previousOpenAtOrBelow(mask, depth) {
|
|
2500
|
+
const n = mask.length;
|
|
2501
|
+
const nearest = new Int32Array(n + 1).fill(-1);
|
|
2502
|
+
const stack = new Int32Array(n);
|
|
2503
|
+
let top = 0;
|
|
2504
|
+
for (let k = 0; k <= n; k++) {
|
|
2505
|
+
nearest[k] = _topmostAtOrBelow(stack, top, depth, 1, depth[k]);
|
|
2506
|
+
if (k < n && mask[k] === "{") {
|
|
2507
|
+
while (top > 0 && depth[stack[top - 1] + 1] >= depth[k + 1]) top--;
|
|
2508
|
+
stack[top++] = k;
|
|
2246
2509
|
}
|
|
2247
2510
|
}
|
|
2248
|
-
return
|
|
2511
|
+
return nearest;
|
|
2249
2512
|
}
|
|
2250
2513
|
|
|
2514
|
+
function _topmostAtOrBelow(stack, top, depth, shift, limit) {
|
|
2515
|
+
let lo = 0;
|
|
2516
|
+
let hi = top - 1;
|
|
2517
|
+
let found = -1;
|
|
2518
|
+
while (lo <= hi) {
|
|
2519
|
+
const mid = (lo + hi) >> 1;
|
|
2520
|
+
if (depth[stack[mid] + shift] <= limit) {
|
|
2521
|
+
found = stack[mid];
|
|
2522
|
+
lo = mid + 1;
|
|
2523
|
+
} else hi = mid - 1;
|
|
2524
|
+
}
|
|
2525
|
+
return found;
|
|
2526
|
+
}
|
|
2251
2527
|
// The non-escape branch excludes `\` so the two branches can never both
|
|
2252
2528
|
// match the same character. With a bare `.`, escape pairs before a quote
|
|
2253
2529
|
// the anchor then rejects backtrack exponentially — 60 characters of an
|
|
@@ -2256,8 +2532,7 @@ function _bracedSpan(code, from) {
|
|
|
2256
2532
|
//
|
|
2257
2533
|
// It excludes the line terminators too, because a character class — unlike
|
|
2258
2534
|
// `.` — matches them, which would widen the literal past its own line.
|
|
2259
|
-
// An odd trailing backslash run therefore no longer matches
|
|
2260
|
-
// shares the escape rule and never yields one.
|
|
2535
|
+
// An odd trailing backslash run therefore no longer matches.
|
|
2261
2536
|
const _TEXT_LITERAL_CHILD_RE =
|
|
2262
2537
|
/^(["'`])((?:\\.|(?!\1)[^\\\r\n\u2028\u2029])*)\1$/;
|
|
2263
2538
|
|
|
@@ -2273,16 +2548,23 @@ function _textLiteralOccurrences(codes, masks) {
|
|
|
2273
2548
|
// not read as markup. Both masks preserve offsets, so the child is read
|
|
2274
2549
|
// back from the kept-strings source at the very same index.
|
|
2275
2550
|
const mask = masks[f];
|
|
2551
|
+
const index = _jsxScanIndex(mask);
|
|
2552
|
+
const lineAt = _lineCounter(code);
|
|
2553
|
+
// Unclosed tags share the next closing `>`, and so share its child: read
|
|
2554
|
+
// each child once, or `<Text <Text … >` re-reads it per tag.
|
|
2555
|
+
const readEnds = new Set();
|
|
2276
2556
|
const re = /<Text\b/g;
|
|
2277
2557
|
let m;
|
|
2278
2558
|
while ((m = re.exec(mask)) !== null) {
|
|
2279
|
-
const end =
|
|
2280
|
-
if (end === -1) continue;
|
|
2559
|
+
const end = index.tagEnd(m.index);
|
|
2560
|
+
if (end === -1 || readEnds.has(end)) continue;
|
|
2561
|
+
readEnds.add(end);
|
|
2281
2562
|
let i = end + 1;
|
|
2282
2563
|
while (i < code.length && /\s/.test(code[i])) i++;
|
|
2283
2564
|
let text = null;
|
|
2284
2565
|
if (code[i] === "{") {
|
|
2285
|
-
const
|
|
2566
|
+
const close = index.braceClose(i);
|
|
2567
|
+
const inner = close === -1 ? null : code.slice(i + 1, close);
|
|
2286
2568
|
const lit = inner && inner.trim().match(_TEXT_LITERAL_CHILD_RE);
|
|
2287
2569
|
// A template carrying `${…}` interpolates a value, so it is not a literal.
|
|
2288
2570
|
if (lit && !(lit[1] === "`" && lit[2].includes("${"))) text = lit[2];
|
|
@@ -2294,7 +2576,7 @@ function _textLiteralOccurrences(codes, masks) {
|
|
|
2294
2576
|
text = text.trim();
|
|
2295
2577
|
if (!_isAuthoredCopy(text) || byText.has(text)) continue;
|
|
2296
2578
|
// Split-impl carries the same copy twice; report the string, not the file.
|
|
2297
|
-
byText.set(text,
|
|
2579
|
+
byText.set(text, lineAt(i));
|
|
2298
2580
|
}
|
|
2299
2581
|
}
|
|
2300
2582
|
return [...byText.entries()].map(([text, line]) => ({ text, line }));
|