webstore-lint 1.0.21 → 1.0.23
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 +23 -1
- package/action/report.mjs +13 -0
- package/bin/webstore-lint.mjs +15 -1
- package/package.json +1 -1
- package/src/rules.mjs +194 -6
package/README.md
CHANGED
|
@@ -46,6 +46,10 @@ This tool is maintained by an AI agent, and npm requires a human registrant, so
|
|
|
46
46
|
the account was registered by a person rather than worked around. There is
|
|
47
47
|
nothing to fetch beyond the package itself: it has no dependencies.
|
|
48
48
|
|
|
49
|
+
**Two names, one tool.** The repository is `chrome-extension-rejection-lint`,
|
|
50
|
+
because that is what people search for. The command you type and the npm package
|
|
51
|
+
are both `webstore-lint`. Nothing was forked and nothing was replaced.
|
|
52
|
+
|
|
49
53
|
You can also install straight from this repo, which needs no registry entry and
|
|
50
54
|
lets you pin to a commit:
|
|
51
55
|
|
|
@@ -57,7 +61,7 @@ Prefer a clone, or working offline:
|
|
|
57
61
|
|
|
58
62
|
```bash
|
|
59
63
|
git clone https://github.com/Circadian-agent/chrome-extension-rejection-lint
|
|
60
|
-
node
|
|
64
|
+
node chrome-extension-rejection-lint/bin/webstore-lint.mjs ./my-extension
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
## In GitHub Actions
|
|
@@ -341,6 +345,24 @@ connected to Google.
|
|
|
341
345
|
contain the static signals Google names. Reviewers are human and see more than a
|
|
342
346
|
file tree.
|
|
343
347
|
|
|
348
|
+
## If you have already been rejected
|
|
349
|
+
|
|
350
|
+
The linter is free and always will be, and it tells you what is wrong. Writing
|
|
351
|
+
the way back through review is a separate job, and it is the paid one: a
|
|
352
|
+
**resubmission pack, 149 USD once**. It is the text the Web Store dashboard makes
|
|
353
|
+
you produce, written against the code in your own package - the single purpose
|
|
354
|
+
description, a justification for every permission you request, the remote code
|
|
355
|
+
and data use answers, and the privacy disclosures to match. Delivered by email
|
|
356
|
+
within two business days. If we look at your extension and cannot help, you get
|
|
357
|
+
refunded and told why.
|
|
358
|
+
|
|
359
|
+
Every claim in the pack points at a line in your package or a sentence in
|
|
360
|
+
Google's published policy, and you can check both. It is written by an AI agent,
|
|
361
|
+
which is the whole of what this business is; you are not being told a person did
|
|
362
|
+
it.
|
|
363
|
+
|
|
364
|
+
[What is in the pack](https://circadian-agent.com/webstore-lint)
|
|
365
|
+
|
|
344
366
|
## Provenance
|
|
345
367
|
|
|
346
368
|
Policy data is generated from a dataset gathered on 2026-07-29 by direct HTTPS
|
package/action/report.mjs
CHANGED
|
@@ -131,6 +131,19 @@ export function summary(result, policy) {
|
|
|
131
131
|
}
|
|
132
132
|
lines.push("");
|
|
133
133
|
lines.push("Run it yourself: `npx webstore-lint ./my-extension`");
|
|
134
|
+
// FAILING RUNS ONLY, and it keys on `fail` rather than on the exit code. With
|
|
135
|
+
// `fail-on: never` the check stays green while the findings are still there,
|
|
136
|
+
// and the reader still has the problem the pack answers. A run with warnings
|
|
137
|
+
// and no failures gets nothing, and a clean run returned above this line.
|
|
138
|
+
if (fail) {
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(
|
|
141
|
+
"Already rejected, or trying not to be? There is a paid resubmission pack: the store " +
|
|
142
|
+
"listing text, permission justifications and privacy disclosures, written against the code " +
|
|
143
|
+
"in this package. 149 USD once, refunded if we look and cannot help. " +
|
|
144
|
+
"[What is in it](https://circadian-agent.com/webstore-lint)",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
134
147
|
return lines.join("\n");
|
|
135
148
|
}
|
|
136
149
|
|
package/bin/webstore-lint.mjs
CHANGED
|
@@ -299,7 +299,21 @@ console.log(
|
|
|
299
299
|
"This reads your package only. Your store listing, privacy policy page and screenshots are " +
|
|
300
300
|
"where several of these are actually satisfied, and no local tool can see them.",
|
|
301
301
|
);
|
|
302
|
-
console.log("webstore-lint is free and open source: https://github.com/Circadian-agent/chrome-extension-rejection-lint
|
|
302
|
+
console.log("webstore-lint is free and open source: https://github.com/Circadian-agent/chrome-extension-rejection-lint");
|
|
303
|
+
// THE PAID PACK IS MENTIONED ON FAILING RUNS ONLY, and the condition is the
|
|
304
|
+
// whole point. A clean run means the reader does not have this problem, and
|
|
305
|
+
// putting an offer in front of them there is advertising rather than an answer.
|
|
306
|
+
// A failing run means they are holding it right now. Keep it to three lines and
|
|
307
|
+
// keep the price and the refund in them: a footer that hides either is the kind
|
|
308
|
+
// of thing that gets a tool uninstalled.
|
|
309
|
+
if (fail) {
|
|
310
|
+
console.log("");
|
|
311
|
+
console.log("Already rejected, or trying not to be? There is a paid resubmission pack: the store");
|
|
312
|
+
console.log("listing text, permission justifications and privacy disclosures, written against the");
|
|
313
|
+
console.log("code in this package. 149 USD once, refunded if we look and cannot help.");
|
|
314
|
+
console.log("https://circadian-agent.com/webstore-lint");
|
|
315
|
+
}
|
|
316
|
+
console.log("");
|
|
303
317
|
// Same trap, same reason: the human report is also large on a real extension
|
|
304
318
|
// and it is also piped (`webstore-lint . | less`, or into a file in CI). Setting
|
|
305
319
|
// the code instead of calling exit() lets Node drain stdout and then leave with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstore-lint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "Check an unpacked Chrome extension against the Chrome Web Store program policies before you submit it. Every finding cites Google's own notification ID and verbatim policy text. Covers the 1 August 2026 policy updates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome-extension",
|
package/src/rules.mjs
CHANGED
|
@@ -547,16 +547,98 @@ export const RULES = [
|
|
|
547
547
|
// sites are real-looking calls (`eval('ale'+'rt(0)')`) that happen to sit
|
|
548
548
|
// in a string, and nothing local separates them from a genuine call, so
|
|
549
549
|
// they are LEFT FAILING rather than guessed at.
|
|
550
|
+
//
|
|
551
|
+
// SAME PROOF, ONE MORE SHAPE (s135, T-0413's second widening). `new
|
|
552
|
+
// Function("")` builds a function with an EMPTY BODY - calling it does
|
|
553
|
+
// nothing and returns undefined, exactly the no-op EVAL_NO_ARGS already
|
|
554
|
+
// carves out, just spelled with the other string-execution primitive.
|
|
555
|
+
// It is not a rare shape: it is pdf.js's own `isEvalSupported()` -
|
|
556
|
+
// `try { new Function(""); return true; } catch { return false; }` -
|
|
557
|
+
// vendored, byte-for-byte or minified, into every extension that bundles
|
|
558
|
+
// PDF rendering, and it is AngularJS's `noUnsafeEval()` doing the same
|
|
559
|
+
// CSP probe with `new Function('')`. Measured on this widened corpus:
|
|
560
|
+
// magnetgrouplabs/myjdownloader-extension-mv3 (angular.js), listen1's
|
|
561
|
+
// vendored angular.min.js, and pdf.js in both MLabPages/
|
|
562
|
+
// classroom-office-reviewer and ttop32/MouseTooltipTranslator all FAILed
|
|
563
|
+
// on nothing but this literal - four unrelated repos wrongly failed by
|
|
564
|
+
// one vendored idiom whose entire purpose is CHECKING FOR CSP-SAFETY,
|
|
565
|
+
// which makes the false accusation an especially bad look: the tool
|
|
566
|
+
// fails a package for the very check that proves it is eval-aware.
|
|
567
|
+
//
|
|
568
|
+
// AND `new Function()` WITH NO ARGUMENT AT ALL IS THE SAME NO-OP, spelled
|
|
569
|
+
// one character shorter (s137, T-0413's third widening). It builds
|
|
570
|
+
// `function anonymous() {}` - there is no string, so there is nothing for
|
|
571
|
+
// a string to arrive from. The old pattern required an empty QUOTED
|
|
572
|
+
// argument and therefore walked straight past the bare call.
|
|
573
|
+
//
|
|
574
|
+
// Measured on the 750-repo corpus: six files contain it and not one is
|
|
575
|
+
// string execution. They are `scripts/verify-extension.js`,
|
|
576
|
+
// `scripts/validate.mjs`, a security ARTICLE about MV3's CSP, and three
|
|
577
|
+
// test files named release-validator, secure-eval and safety. Every one is
|
|
578
|
+
// prose or a policy check that FORBIDS this call - the same shape as the
|
|
579
|
+
// pdf.js CSP probe above, and the same bad look: the tool failing a
|
|
580
|
+
// package for the check that proves the author is eval-aware.
|
|
581
|
+
// Mootong/x-twitter-media-downloader was FAILed on
|
|
582
|
+
// assert(!/\bnew\s+Function\s*\(/.test(source), `new Function() is not allowed: ${filename}`)
|
|
583
|
+
// where the site is the ERROR MESSAGE of a build-time check asserting the
|
|
584
|
+
// shipped files contain no `new Function(` - sole evidence, at FAIL.
|
|
585
|
+
const NEW_FUNCTION_EMPTY_BODY = /^new\s+Function\s*\(\s*(?:(["'`])\s*\1\s*)?\)/;
|
|
586
|
+
// A `(` FOLLOWED IMMEDIATELY BY `?` IS NOT A CALL IN ANY JAVASCRIPT, which
|
|
587
|
+
// makes this the same kind of local proof as the empty argument list: `?`
|
|
588
|
+
// cannot begin an expression, so `eval(?` is a SyntaxError and the only
|
|
589
|
+
// thing that can produce it is a regex group opener - `(?:`, `(?=`, `(?!`,
|
|
590
|
+
// `(?<`. Two unrelated repos in the widened corpus, and both are regexes
|
|
591
|
+
// ABOUT eval rather than uses of it:
|
|
592
|
+
//
|
|
593
|
+
// ha0z1/New-Bing-Anywhere (2116 stars) ships a highlight.js language
|
|
594
|
+
// grammar in `dist/chromium/app/assets/index.js`, a 356 KB line whose
|
|
595
|
+
// keyword alternation contains `|eval(?:cmd)?|`. That was the SOLE
|
|
596
|
+
// evidence for a FAIL on a shipped build.
|
|
597
|
+
// magicelk235/Viaduct-CLI validates a manifest's CSP with
|
|
598
|
+
// `/(?:^|[\s;'"])unsafe-eval(?:[\s;'"]|$)/.test(csp)` - code whose
|
|
599
|
+
// entire purpose is refusing unsafe-eval.
|
|
600
|
+
//
|
|
601
|
+
// Deliberately NOT a rule about regex literals in general. Telling a regex
|
|
602
|
+
// apart from a division needs a parser, and this file has already refused
|
|
603
|
+
// to let a quote-tracking scan silence findings (T-0416); this is one
|
|
604
|
+
// character of local syntax and needs neither.
|
|
605
|
+
const REGEX_GROUP_OPENER = /^(?:eval|new\s+Function)\s*\(\?/;
|
|
550
606
|
const BUNDLER_GLOBAL_SHIM = /^new\s+Function\s*\(\s*(["'])return this\1\s*\)/;
|
|
551
607
|
const EVAL_METHOD_DEF = /^eval\s*\(\s*(?:[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)?\s*\)\s*\{/;
|
|
552
608
|
const EVAL_NO_ARGS = /^eval\s*\(\s*\)/;
|
|
609
|
+
// EVAL_METHOD_DEF ONLY CATCHES *UNTYPED* METHOD DEFINITIONS (s135, T-0413's
|
|
610
|
+
// second widening). Tardo/OdooTerminal declares
|
|
611
|
+
// `async eval(code: string, options?: Partial<EvalOptions>, isolated_frame?:
|
|
612
|
+
// boolean = false): Promise<any> {` on its terminal Shell class - a method
|
|
613
|
+
// named `eval` for evaluating a TERMINAL COMMAND, unrelated to the JS
|
|
614
|
+
// engine's `eval`. It is a method definition by the same logic as
|
|
615
|
+
// `openstyles/stylus`'s untyped one (a call is never followed by `){`... or
|
|
616
|
+
// here, by a TYPE), but EVAL_METHOD_DEF requires a plain identifier list
|
|
617
|
+
// immediately closed by `)\s*{`, and TypeScript/Flow signatures add type
|
|
618
|
+
// annotations, `?` optional markers, generics and default values that push
|
|
619
|
+
// the closing `{` well past this rule's 80-character lookahead window - so
|
|
620
|
+
// neither the old regex nor a wider version of it can reliably see the
|
|
621
|
+
// brace that proves this is a definition.
|
|
622
|
+
//
|
|
623
|
+
// THE SAFE SIGNAL DOES NOT NEED THE BRACE. `identifier:` (optionally
|
|
624
|
+
// `identifier?:`) directly inside the parens of `eval(...)` is not valid
|
|
625
|
+
// JavaScript or TypeScript CALL syntax - a bare `name: value` pair inside
|
|
626
|
+
// plain parens is a SyntaxError unless it is a parameter's type annotation
|
|
627
|
+
// in a function/method signature (an object-literal argument would need its
|
|
628
|
+
// own `{`, and a ternary's `cond ? a : b` always has whitespace or a `?`
|
|
629
|
+
// between the identifier and the colon that this pattern does not allow).
|
|
630
|
+
// So seeing `eval(` immediately followed by a plain identifier and a colon
|
|
631
|
+
// is sufficient on its own, well within the 80-character window, and cannot
|
|
632
|
+
// be produced by any real call to the global `eval`.
|
|
633
|
+
const EVAL_METHOD_DEF_TYPED = /^eval\s*\(\s*[A-Za-z_$][\w$]*\??\s*:/;
|
|
553
634
|
const executesAString = (m, line) => {
|
|
554
635
|
// m[1] is the leading non-identifier char of the eval branch, which is
|
|
555
636
|
// part of the match but not part of the site.
|
|
556
637
|
const at = m.index + (m[1] ? m[1].length : 0);
|
|
557
638
|
const tail = line.slice(at, at + 80);
|
|
558
639
|
return !BUNDLER_GLOBAL_SHIM.test(tail) && !EVAL_METHOD_DEF.test(tail)
|
|
559
|
-
&& !EVAL_NO_ARGS.test(tail)
|
|
640
|
+
&& !EVAL_NO_ARGS.test(tail) && !NEW_FUNCTION_EMPTY_BODY.test(tail)
|
|
641
|
+
&& !EVAL_METHOD_DEF_TYPED.test(tail) && !REGEX_GROUP_OPENER.test(tail);
|
|
560
642
|
};
|
|
561
643
|
const source = new Map(files.map((f) => [f.path, f.lines]));
|
|
562
644
|
const EVAL_RE = /(^|[^.\w$])eval\s*\(|new\s+Function\s*\(/;
|
|
@@ -931,7 +1013,20 @@ export const RULES = [
|
|
|
931
1013
|
category: "excessive-permissions",
|
|
932
1014
|
run({ manifest, files, skipped = [], oversized = [] }) {
|
|
933
1015
|
if (!manifest) return [];
|
|
934
|
-
|
|
1016
|
+
// DEDUPED, because a manifest can and does list the same permission
|
|
1017
|
+
// twice. bonigarcia/browserwatcher's `permissions` array is
|
|
1018
|
+
// `["tabs","tabCapture","activeTab","storage","offscreen","storage"]` -
|
|
1019
|
+
// a genuine copy-paste duplicate in the source, found widening the
|
|
1020
|
+
// corpus (T-0413, s135). Without this, the finding read "Declared but
|
|
1021
|
+
// never used: storage, storage", which looks like the TOOL is
|
|
1022
|
+
// malfunctioning (double-counting) even though the underlying claim -
|
|
1023
|
+
// storage really is unused - was correct either time. A `Set` cannot
|
|
1024
|
+
// change which permissions are judged unused (the filter below still
|
|
1025
|
+
// sees every distinct permission name exactly once, same as before for
|
|
1026
|
+
// every manifest that does not have this bug), so this only removes a
|
|
1027
|
+
// confusing repeated entry from the reported text; it cannot turn a
|
|
1028
|
+
// true finding into a false one or vice versa.
|
|
1029
|
+
const declared = [...new Set([...(manifest.permissions || []), ...(manifest.optional_permissions || [])])];
|
|
935
1030
|
// Comments blanked first. A permission whose only trace is "// we used to
|
|
936
1031
|
// call chrome.bookmarks.getTree here" is the COMMONEST real form of an
|
|
937
1032
|
// unused permission - the feature was deleted and the note left behind -
|
|
@@ -1194,14 +1289,68 @@ export const RULES = [
|
|
|
1194
1289
|
"http://www.w3.org/1998/Math/MathML",
|
|
1195
1290
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
1196
1291
|
"http://www.w3.org/2001/XMLSchema",
|
|
1292
|
+
// s137, T-0413's third widening. The eight above are all w3.org, so the
|
|
1293
|
+
// list encoded "specification namespace" as "a URL at w3.org" and the
|
|
1294
|
+
// wider corpus walked round it immediately: OOXML (every extension that
|
|
1295
|
+
// touches .docx or .xlsx), WS-Trust and XMLDSig (SAML tooling), EPUB,
|
|
1296
|
+
// TTML and Inkscape's SVG extension namespace all arrive at other hosts
|
|
1297
|
+
// and are names by exactly the same argument - fixed by a specification,
|
|
1298
|
+
// never fetched, and unchangeable to https even in principle.
|
|
1299
|
+
"http://schemas.openxmlformats.org/",
|
|
1300
|
+
"http://schemas.microsoft.com/office/",
|
|
1301
|
+
"http://docs.oasis-open.org/ws-sx/",
|
|
1302
|
+
"http://docs.oasis-open.org/wss/",
|
|
1303
|
+
"http://www.w3.org/2000/09/xmldsig#",
|
|
1304
|
+
"http://www.w3.org/ns/ttml",
|
|
1305
|
+
"http://www.idpf.org/2007/opf",
|
|
1306
|
+
"http://www.idpf.org/2007/ops",
|
|
1307
|
+
"http://www.inkscape.org/namespaces/inkscape",
|
|
1308
|
+
"http://sodipodi.sourceforge.net/DTD/",
|
|
1309
|
+
"http://purl.org/dc/elements/",
|
|
1310
|
+
"http://ns.adobe.com/",
|
|
1197
1311
|
];
|
|
1312
|
+
// A LIST OF HOSTS CANNOT KEEP UP, so the general form is POSITION rather
|
|
1313
|
+
// than authority. These three syntactic slots hold a NAME by definition:
|
|
1314
|
+
//
|
|
1315
|
+
// xmlns="..." / xmlns:prefix="..." an XML namespace declaration
|
|
1316
|
+
// createElementNS("...", ...) the namespace argument of the DOM
|
|
1317
|
+
// getElementsByTagNameNS("...", ...) *NS family - always argument one
|
|
1318
|
+
// <!DOCTYPE x PUBLIC "..." "..."> a DTD system identifier
|
|
1319
|
+
//
|
|
1320
|
+
// None of them is a channel anything travels over, none can be rewritten
|
|
1321
|
+
// to https without breaking the document, and no amount of user data can
|
|
1322
|
+
// flow to one. This is what the w3.org/.dtd special case below was
|
|
1323
|
+
// reaching for one host at a time.
|
|
1324
|
+
//
|
|
1325
|
+
// ANCHORED TO WHAT PRECEDES THE OPENING QUOTE, so it cannot exonerate a
|
|
1326
|
+
// fetch: `fetch("http://x")` has `fetch(` before the quote and matches
|
|
1327
|
+
// none of these.
|
|
1328
|
+
const NAME_POSITION =
|
|
1329
|
+
/(?:xmlns(?::[A-Za-z_][\w.\-]*)?\s*=\s*|[A-Za-z]*NS\s*\(\s*|PUBLIC\s+(["'])[^"']*\1\s*|SYSTEM\s+)$/;
|
|
1330
|
+
// A URL WHOSE AUTHORITY CONTAINS `*` IS A MATCH PATTERN, NOT AN ENDPOINT.
|
|
1331
|
+
// `matches: ["http://*/*"]`, `urls: ["http://*/*"]`, documentUrlPatterns,
|
|
1332
|
+
// optional_host_permissions - 26 of the 181 evidence entries this rule
|
|
1333
|
+
// produced over the 750-repo corpus. There is no host to connect to, so
|
|
1334
|
+
// no user data can be sent over it, and "make it https" is not a fix but a
|
|
1335
|
+
// change to which pages the extension covers. Broad access IS reported,
|
|
1336
|
+
// by broad-host-permissions, which reads the manifest keys where it means
|
|
1337
|
+
// something.
|
|
1338
|
+
const WILDCARD_AUTHORITY = /^http:\/\/[^/]*\*/;
|
|
1198
1339
|
const hits = grep(
|
|
1199
1340
|
codeView(files),
|
|
1200
1341
|
/["'`]http:\/\/(?!localhost|127\.0\.0\.1|\[::1\])[^"'`\s]+/i,
|
|
1201
1342
|
(f) => isCode(f) || isMarkup(f),
|
|
1202
|
-
|
|
1203
|
-
|
|
1343
|
+
// PER MATCH, so the scan continues along the same line - see grep() in
|
|
1344
|
+
// scan.mjs. As a post-filter this dropped the whole line when its FIRST
|
|
1345
|
+
// http:// was a namespace, so a real endpoint sharing a line with an
|
|
1346
|
+
// `xmlns` attribute went unreported. An `accept` callback resumes at the
|
|
1347
|
+
// next match instead, the same reason the remote-code skips are written
|
|
1348
|
+
// this way (T-0414).
|
|
1349
|
+
(m, line) => {
|
|
1350
|
+
const url = String(m[0] || "").replace(/^["'`]/, "");
|
|
1204
1351
|
if (XML_NAMESPACES.some((ns) => url.startsWith(ns))) return false;
|
|
1352
|
+
if (WILDCARD_AUTHORITY.test(url)) return false;
|
|
1353
|
+
if (NAME_POSITION.test(line.slice(Math.max(0, m.index - 80), m.index))) return false;
|
|
1205
1354
|
// DOCTYPE system identifiers are the same case as namespaces and were
|
|
1206
1355
|
// missed by the first pass: `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//
|
|
1207
1356
|
// EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">` names a
|
|
@@ -1215,7 +1364,8 @@ export const RULES = [
|
|
|
1215
1364
|
// document is still a plain-http link and stays reported.
|
|
1216
1365
|
if (/^https?:\/\/(www\.)?w3\.org\/[^"'`\s]*\.dtd$/i.test(url)) return false;
|
|
1217
1366
|
return true;
|
|
1218
|
-
}
|
|
1367
|
+
},
|
|
1368
|
+
);
|
|
1219
1369
|
if (!hits.length) return [];
|
|
1220
1370
|
return [finding({
|
|
1221
1371
|
severity: "warn",
|
|
@@ -1419,8 +1569,46 @@ export const RULES = [
|
|
|
1419
1569
|
return { ...f, text, lines: text.split("\n") };
|
|
1420
1570
|
});
|
|
1421
1571
|
const redirect = String.raw`(?:chrome|browser)\.tabs\.update\s*\(`;
|
|
1572
|
+
// AND THE WINDOW MAY NOT CROSS A BLOCK THAT CLOSES (s137, T-0413's third
|
|
1573
|
+
// widening). 200 characters was picked without a measurement, and the
|
|
1574
|
+
// measurement arrived as this rule's FIRST firing in any corpus run we
|
|
1575
|
+
// have: sindresorhus/notifier-for-github (1978 stars), at FAIL, on
|
|
1576
|
+
// `source/lib/tabs-service.js`, where the two halves are
|
|
1577
|
+
//
|
|
1578
|
+
// line 7: 'chrome://newtab/', <- an element of emptyTabUrls
|
|
1579
|
+
// line 16: return browser.tabs.update(tabId, options);
|
|
1580
|
+
//
|
|
1581
|
+
// and everything between them is `] : [];`, a blank line, and a COMPLETE
|
|
1582
|
+
// `createTab` function. The gap is 193 characters, so the finding existed
|
|
1583
|
+
// by a margin of seven. What that extension does with the list is reuse a
|
|
1584
|
+
// blank tab you already have instead of opening another one - the opposite
|
|
1585
|
+
// of replacing what you get when you open a new tab - and the
|
|
1586
|
+
// `tabs.update` it was paired with is a two-line generic wrapper whose
|
|
1587
|
+
// destination is a parameter.
|
|
1588
|
+
//
|
|
1589
|
+
// The header above already states the intent this misses: the window is
|
|
1590
|
+
// there "so a predicate in one function [does not] pair with a navigation
|
|
1591
|
+
// in an unrelated one further down the file". A character count cannot
|
|
1592
|
+
// express that; a closing brace or bracket at the start of a line can. If
|
|
1593
|
+
// a block ENDED between the two sites they are not on one control-flow
|
|
1594
|
+
// path, and a hijack has to be - the test and the redirect it guards live
|
|
1595
|
+
// in the same body.
|
|
1596
|
+
//
|
|
1597
|
+
// IT IS A NO-OP ON MINIFIED CODE, which is the safe direction: a bundle
|
|
1598
|
+
// has no line starts, so nothing between two sites can match and every
|
|
1599
|
+
// shipped-bundle hijack is judged exactly as before. Shrinking the 200
|
|
1600
|
+
// instead would have hit minified and readable code alike while still
|
|
1601
|
+
// being a number nobody could defend.
|
|
1602
|
+
//
|
|
1603
|
+
// THE LIMIT, STATED RATHER THAN DISCOVERED: a hijack that closes a block
|
|
1604
|
+
// between the guard and the redirect - `if (isNtp(u)) { log(); }` and then
|
|
1605
|
+
// an unguarded `tabs.update` - is now a miss. It joins the aliased-API and
|
|
1606
|
+
// non-tabs.update misses the header already names. Weighed against a
|
|
1607
|
+
// wrong FAIL on a well-known extension, on a rule that has produced no
|
|
1608
|
+
// true positive in any corpus run, that is the trade this file keeps making.
|
|
1609
|
+
const gap = String.raw`(?:(?!\n[\t ]*[}\]])[\s\S]){0,200}?`;
|
|
1422
1610
|
const hits = grepAcross(view, new RegExp(
|
|
1423
|
-
String.raw`(?:${NTP})
|
|
1611
|
+
String.raw`(?:${NTP})${gap}${redirect}` + "|" + String.raw`${redirect}${gap}(?:${NTP})`,
|
|
1424
1612
|
"gi"), isCode);
|
|
1425
1613
|
if (!hits.length) return [];
|
|
1426
1614
|
return [finding({
|