Bug 1888491 - Follow-up: add an exception for the debug tool in browser_all_files_ref...
[gecko.git] / browser / base / content / test / static / browser_all_files_referenced.js
blob75ab7f7bf12be13dfac94cace66fe99bf2efece5
1 /* Any copyright is dedicated to the Public Domain.
2  * http://creativecommons.org/publicdomain/zero/1.0/ */
4 // Note to run this test similar to try server, you need to run:
5 // ./mach package
6 // ./mach mochitest --appname dist <path to test>
8 // Slow on asan builds.
9 requestLongerTimeout(5);
11 var isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
13 // This list should contain only path prefixes. It is meant to stop the test
14 // from reporting things that *are* referenced, but for which the test can't
15 // find any reference because the URIs are constructed programatically.
16 // If you need to allowlist specific files, please use the 'allowlist' object.
17 var gExceptionPaths = [
18   "resource://app/defaults/settings/blocklists/",
19   "resource://app/defaults/settings/security-state/",
20   "resource://app/defaults/settings/main/",
21   "resource://app/defaults/preferences/",
22   "resource://gre/modules/commonjs/",
23   "resource://gre/defaults/pref/",
25   // These chrome resources are referenced using relative paths from JS files.
26   "chrome://global/content/certviewer/components/",
28   // https://github.com/mozilla/activity-stream/issues/3053
29   "chrome://activity-stream/content/data/content/tippytop/images/",
30   "chrome://activity-stream/content/data/content/tippytop/favicons/",
31   // These resources are referenced by messages delivered through Remote Settings
32   "chrome://activity-stream/content/data/content/assets/remote/",
33   "chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user-cn.svg",
34   "chrome://activity-stream/content/data/content/assets/mobile-download-qr-existing-user-cn.svg",
35   "chrome://activity-stream/content/data/content/assets/person-typing.svg",
36   "chrome://browser/content/assets/moz-vpn.svg",
37   "chrome://browser/content/assets/vpn-logo.svg",
38   "chrome://browser/content/assets/focus-promo.png",
39   "chrome://browser/content/assets/klar-qr-code.svg",
41   // toolkit/components/pdfjs/content/build/pdf.js
42   "resource://pdf.js/web/images/",
44   // Exclude the form autofill path that has been moved out of the extensions to
45   // toolkit, see bug 1691821.
46   "resource://gre-resources/autofill/",
47   // Localization file added programatically in FormAutofillUtils.sys.mjs
48   "resource://gre/localization/en-US/toolkit/formautofill",
50   // Exclude all search-extensions because they aren't referenced by filename
51   "resource://search-extensions/",
53   // Exclude all services-automation because they are used through webdriver
54   "resource://gre/modules/services-automation/",
56   // Paths from this folder are constructed in NetErrorParent.sys.mjs based on
57   // the type of cert or net error the user is encountering.
58   "chrome://global/content/neterror/supportpages/",
60   // Points to theme preview images, which are defined in browser/ but only used
61   // in toolkit/mozapps/extensions/content/aboutaddons.js.
62   "resource://usercontext-content/builtin-themes/",
64   // Page data schemas are referenced programmatically.
65   "chrome://browser/content/pagedata/schemas/",
67   // Nimbus schemas are referenced programmatically.
68   "resource://nimbus/schemas/",
70   // Normandy schemas are referenced programmatically.
71   "resource://normandy/schemas/",
73   // ASRouter schemas are referenced programmatically.
74   "chrome://browser/content/asrouter/schemas/",
76   // Localization file added programatically in FeatureCallout.sys.mjs
77   "resource://app/localization/en-US/browser/featureCallout.ftl",
79   // Localization file added programatically in ContentAnalysis.sys.mjs
80   "resource://gre/localization/en-US/toolkit/contentanalysis/",
82   // CSS files are referenced inside JS in an html template
83   "chrome://browser/content/aboutlogins/components/",
85   // Strip on Share parameter lists
86   "chrome://global/content/antitracking/",
89 // These are not part of the omni.ja file, so we find them only when running
90 // the test on a non-packaged build.
91 if (AppConstants.platform == "macosx") {
92   gExceptionPaths.push("resource://gre/res/cursors/");
93   gExceptionPaths.push("resource://gre/res/touchbar/");
96 if (AppConstants.MOZ_BACKGROUNDTASKS) {
97   // These preferences are active only when we're in background task mode.
98   gExceptionPaths.push("resource://gre/defaults/backgroundtasks/");
99   gExceptionPaths.push("resource://app/defaults/backgroundtasks/");
100   // `BackgroundTask_*.sys.mjs` are loaded at runtime by `app --backgroundtask id ...`.
101   gExceptionPaths.push("resource://gre/modules/backgroundtasks/");
102   gExceptionPaths.push("resource://app/modules/backgroundtasks/");
105 if (AppConstants.NIGHTLY_BUILD) {
106   // This is nightly-only debug tool.
107   gExceptionPaths.push(
108     "chrome://browser/content/places/interactionsViewer.html"
109   );
112 // Each allowlist entry should have a comment indicating which file is
113 // referencing the listed file in a way that the test can't detect, or a
114 // bug number to remove or use the file if it is indeed currently unreferenced.
115 var allowlist = [
116   // security/manager/pki/resources/content/device_manager.js
117   { file: "chrome://pippki/content/load_device.xhtml" },
119   // The l10n build system can't package string files only for some platforms.
120   // See bug 1339424 for why this is hard to fix.
121   {
122     file: "chrome://global/locale/fallbackMenubar.properties",
123     platforms: ["linux", "win"],
124   },
125   {
126     file: "resource://gre/localization/en-US/toolkit/printing/printDialogs.ftl",
127     platforms: ["linux", "macosx"],
128   },
130   // This file is referenced by the build system to generate the
131   // Firefox .desktop entry. See bug 1824327 (and perhaps bug 1526672)
132   {
133     file: "resource://app/localization/en-US/browser/linuxDesktopEntry.ftl",
134   },
136   // toolkit/content/aboutRights-unbranded.xhtml doesn't use aboutRights.css
137   { file: "chrome://global/skin/aboutRights.css", skipUnofficial: true },
139   // devtools/client/inspector/bin/dev-server.js
140   {
141     file: "chrome://devtools/content/inspector/markup/markup.xhtml",
142     isFromDevTools: true,
143   },
145   // used by devtools/client/memory/index.xhtml
146   { file: "chrome://global/content/third_party/d3/d3.js" },
148   // SpiderMonkey parser API, currently unused in browser/ and toolkit/
149   { file: "resource://gre/modules/reflect.sys.mjs" },
151   // extensions/pref/autoconfig/src/nsReadConfig.cpp
152   { file: "resource://gre/defaults/autoconfig/prefcalls.js" },
154   // browser/components/preferences/moreFromMozilla.js
155   // These files URLs are constructed programatically at run time.
156   {
157     file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple.svg",
158   },
159   {
160     file: "chrome://browser/content/preferences/more-from-mozilla-qr-code-simple-cn.svg",
161   },
163   { file: "resource://gre/greprefs.js" },
165   // layout/mathml/nsMathMLChar.cpp
166   { file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties" },
167   { file: "resource://gre/res/fonts/mathfontUnicode.properties" },
169   // toolkit/mozapps/extensions/AddonContentPolicy.cpp
170   { file: "resource://gre/localization/en-US/toolkit/global/cspErrors.ftl" },
172   // The l10n build system can't package string files only for some platforms.
173   {
174     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/accessible.properties",
175     platforms: ["linux", "win"],
176   },
177   {
178     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/intl.properties",
179     platforms: ["linux", "win"],
180   },
181   {
182     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/mac/platformKeys.properties",
183     platforms: ["linux", "win"],
184   },
185   {
186     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/accessible.properties",
187     platforms: ["macosx", "win"],
188   },
189   {
190     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/intl.properties",
191     platforms: ["macosx", "win"],
192   },
193   {
194     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/unix/platformKeys.properties",
195     platforms: ["macosx", "win"],
196   },
197   {
198     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/accessible.properties",
199     platforms: ["linux", "macosx"],
200   },
201   {
202     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/intl.properties",
203     platforms: ["linux", "macosx"],
204   },
205   {
206     file: "resource://gre/chrome/en-US/locale/en-US/global-platform/win/platformKeys.properties",
207     platforms: ["linux", "macosx"],
208   },
210   // Files from upstream library
211   { file: "resource://pdf.js/web/debugger.mjs" },
212   { file: "resource://pdf.js/web/debugger.css" },
214   // Starting from here, files in the allowlist are bugs that need fixing.
215   // Bug 1339424 (wontfix?)
216   {
217     file: "chrome://browser/locale/taskbar.properties",
218     platforms: ["linux", "macosx"],
219   },
220   // Bug 1344267
221   { file: "chrome://remote/content/marionette/test_dialog.properties" },
222   { file: "chrome://remote/content/marionette/test_dialog.xhtml" },
223   { file: "chrome://remote/content/marionette/test_menupopup.xhtml" },
224   { file: "chrome://remote/content/marionette/test_no_xul.xhtml" },
225   { file: "chrome://remote/content/marionette/test.xhtml" },
226   // Bug 1348559
227   { file: "chrome://pippki/content/resetpassword.xhtml" },
228   // Bug 1337345
229   { file: "resource://gre/modules/Manifest.sys.mjs" },
230   // Bug 1494170
231   // (The references to these files are dynamically generated, so the test can't
232   // find the references)
233   {
234     file: "chrome://devtools/skin/images/aboutdebugging-firefox-aurora.svg",
235     isFromDevTools: true,
236   },
237   {
238     file: "chrome://devtools/skin/images/aboutdebugging-firefox-beta.svg",
239     isFromDevTools: true,
240   },
241   {
242     file: "chrome://devtools/skin/images/aboutdebugging-firefox-release.svg",
243     isFromDevTools: true,
244   },
245   { file: "chrome://devtools/skin/images/next.svg", isFromDevTools: true },
247   // Bug 1526672
248   {
249     file: "resource://app/localization/en-US/browser/touchbar/touchbar.ftl",
250     platforms: ["linux", "win"],
251   },
252   // Referenced by the webcompat system addon for localization
253   { file: "resource://gre/localization/en-US/toolkit/about/aboutCompat.ftl" },
255   // dom/media/mediacontrol/MediaControlService.cpp
256   { file: "resource://gre/localization/en-US/dom/media.ftl" },
258   // dom/xml/nsXMLPrettyPrinter.cpp
259   { file: "resource://gre/localization/en-US/dom/XMLPrettyPrint.ftl" },
261   // tookit/mozapps/update/BackgroundUpdate.sys.mjs
262   {
263     file: "resource://gre/localization/en-US/toolkit/updates/backgroundupdate.ftl",
264   },
266   // Bug 1713242 - referenced by aboutThirdParty.html which is only for Windows
267   {
268     file: "resource://gre/localization/en-US/toolkit/about/aboutThirdParty.ftl",
269     platforms: ["linux", "macosx"],
270   },
271   // Bug 1854618 - referenced by aboutWebauthn.html which is only for Linux and Mac
272   {
273     file: "resource://gre/localization/en-US/toolkit/about/aboutWebauthn.ftl",
274     platforms: ["win", "android"],
275   },
276   // Bug 1973834 - referenced by aboutWindowsMessages.html which is only for Windows
277   {
278     file: "resource://gre/localization/en-US/toolkit/about/aboutWindowsMessages.ftl",
279     platforms: ["linux", "macosx"],
280   },
281   // Bug 1721741:
282   // (The references to these files are dynamically generated, so the test can't
283   // find the references)
284   { file: "chrome://browser/content/screenshots/copied-notification.svg" },
286   // Bug 1875361
287   { file: "chrome://global/content/ml/SummarizerModel.sys.mjs" },
289   // Bug 1886130
290   { file: "chrome://global/content/ml/ModelHub.sys.mjs" },
292   // toolkit/xre/MacRunFromDmgUtils.mm
293   { file: "resource://gre/localization/en-US/toolkit/global/run-from-dmg.ftl" },
295   // Referenced by screenshots extension
296   { file: "chrome://browser/content/screenshots/cancel.svg" },
297   { file: "chrome://browser/content/screenshots/copy.svg" },
298   { file: "chrome://browser/content/screenshots/download.svg" },
299   { file: "chrome://browser/content/screenshots/download-white.svg" },
300     
301   // A debug tool that is only available in Nightly builds, and is accessed
302   // directly by developers via the chrome URI (bug 1888491)
303   { file: "chrome://browser/content/backup/debug.html" },  
306 if (AppConstants.platform != "win") {
307   // toolkit/mozapps/defaultagent/Notification.cpp
308   // toolkit/mozapps/defaultagent/ScheduledTask.cpp
309   // toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
310   // Bug 1854425 - referenced by default browser agent which is not detected
311   allowlist.push({
312     file: "resource://app/localization/en-US/browser/backgroundtasks/defaultagent.ftl",
313   });
315   if (AppConstants.NIGHTLY_BUILD) {
316     // This path is refereneced in nsFxrCommandLineHandler.cpp, which is only
317     // compiled in Windows. This path is allowed so that non-Windows builds
318     // can access the FxR UI via --chrome rather than --fxr (which includes VR-
319     // specific functionality)
320     allowlist.push({ file: "chrome://fxr/content/fxrui.html" });
321   }
324 if (AppConstants.platform == "android") {
325   // The l10n build system can't package string files only for some platforms.
326   // Referenced by aboutGlean.html
327   allowlist.push({
328     file: "resource://gre/localization/en-US/toolkit/about/aboutGlean.ftl",
329   });
332 if (AppConstants.MOZ_UPDATE_AGENT && !AppConstants.MOZ_BACKGROUNDTASKS) {
333   // Task scheduling is only used for background updates right now.
334   allowlist.push({
335     file: "resource://gre/modules/TaskScheduler.sys.mjs",
336   });
339 allowlist = new Set(
340   allowlist
341     .filter(
342       item =>
343         "isFromDevTools" in item == isDevtools &&
344         (!item.skipUnofficial || !AppConstants.MOZILLA_OFFICIAL) &&
345         (!item.platforms || item.platforms.includes(AppConstants.platform))
346     )
347     .map(item => item.file)
350 const ignorableAllowlist = new Set([
351   // The following files are outside of the omni.ja file, so we only catch them
352   // when testing on a non-packaged build.
354   // toolkit/mozapps/extensions/nsBlocklistService.js
355   "resource://app/blocklist.xml",
357   // dom/media/gmp/GMPParent.cpp
358   "resource://gre/gmp-clearkey/0.1/manifest.json",
360   // Bug 1351669 - obsolete test file
361   "resource://gre/res/test.properties",
363 for (let entry of ignorableAllowlist) {
364   allowlist.add(entry);
367 if (!isDevtools) {
368   // services/sync/modules/service.sys.mjs
369   for (let module of [
370     "addons.sys.mjs",
371     "bookmarks.sys.mjs",
372     "forms.sys.mjs",
373     "history.sys.mjs",
374     "passwords.sys.mjs",
375     "prefs.sys.mjs",
376     "tabs.sys.mjs",
377     "extension-storage.sys.mjs",
378   ]) {
379     allowlist.add("resource://services-sync/engines/" + module);
380   }
381   // resource://devtools/shared/worker/loader.js,
382   // resource://devtools/shared/loader/builtin-modules.js
383   if (!AppConstants.ENABLE_WEBDRIVER) {
384     allowlist.add("resource://gre/modules/jsdebugger.sys.mjs");
385   }
388 if (AppConstants.MOZ_CODE_COVERAGE) {
389   allowlist.add(
390     "chrome://remote/content/marionette/PerTestCoverageUtils.sys.mjs"
391   );
394 const gInterestingCategories = new Set([
395   "addon-provider-module",
396   "webextension-modules",
397   "webextension-scripts",
398   "webextension-schemas",
399   "webextension-scripts-addon",
400   "webextension-scripts-content",
401   "webextension-scripts-devtools",
404 var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
405   Ci.nsIChromeRegistry
407 var gChromeMap = new Map();
408 var gOverrideMap = new Map();
409 var gComponentsSet = new Set();
411 // In this map when the value is a Set of URLs, the file is referenced if any
412 // of the files in the Set is referenced.
413 // When the value is null, the file is referenced unconditionally.
414 // When the value is a string, "allowlist-direct" means that we have not found
415 // any reference in the code, but have a matching allowlist entry for this file.
416 // "allowlist" means that the file is indirectly allowlisted, ie. a allowlisted
417 // file causes this file to be referenced.
418 var gReferencesFromCode = new Map();
420 var resHandler = Services.io
421   .getProtocolHandler("resource")
422   .QueryInterface(Ci.nsIResProtocolHandler);
423 var gResourceMap = [];
424 function trackResourcePrefix(prefix) {
425   let uri = Services.io.newURI("resource://" + prefix + "/");
426   gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
428 trackResourcePrefix("gre");
429 trackResourcePrefix("app");
431 function getBaseUriForChromeUri(chromeUri) {
432   let chromeFile = chromeUri + "nonexistentfile.reallynothere";
433   let uri = Services.io.newURI(chromeFile);
434   let fileUri = gChromeReg.convertChromeURL(uri);
435   return fileUri.resolve(".");
438 function trackChromeUri(uri) {
439   gChromeMap.set(getBaseUriForChromeUri(uri), uri);
442 // formautofill registers resource://formautofill/ and
443 // chrome://formautofill/content/ dynamically at runtime.
444 // Bug 1480276 is about addressing this without this hard-coding.
445 trackResourcePrefix("autofill");
446 trackChromeUri("chrome://formautofill/content/");
448 function parseManifest(manifestUri) {
449   return fetchFile(manifestUri.spec).then(data => {
450     for (let line of data.split("\n")) {
451       let [type, ...argv] = line.split(/\s+/);
452       if (type == "content" || type == "skin" || type == "locale") {
453         let chromeUri = `chrome://${argv[0]}/${type}/`;
454         // The webcompat reporter's locale directory may not exist if
455         // the addon is preffed-off, and since it's a hack until we
456         // get bz1425104 landed, we'll just skip it for now.
457         if (chromeUri === "chrome://report-site-issue/locale/") {
458           gChromeMap.set("chrome://report-site-issue/locale/", true);
459         } else {
460           trackChromeUri(chromeUri);
461         }
462       } else if (type == "override" || type == "overlay") {
463         // Overlays aren't really overrides, but behave the same in
464         // that the overlay is only referenced if the original xul
465         // file is referenced somewhere.
466         let os = "os=" + Services.appinfo.OS;
467         if (!argv.some(s => s.startsWith("os=") && s != os)) {
468           gOverrideMap.set(
469             Services.io.newURI(argv[1]).specIgnoringRef,
470             Services.io.newURI(argv[0]).specIgnoringRef
471           );
472         }
473       } else if (type == "category" && gInterestingCategories.has(argv[0])) {
474         gReferencesFromCode.set(argv[2], null);
475       } else if (type == "resource") {
476         trackResourcePrefix(argv[0]);
477       } else if (type == "component") {
478         gComponentsSet.add(argv[1]);
479       }
480     }
481   });
484 // If the given URI is a webextension manifest, extract files used by
485 // any of its APIs (scripts, icons, style sheets, theme images).
486 // Returns the passed in URI if the manifest is not a webextension
487 // manifest, null otherwise.
488 async function parseJsonManifest(uri) {
489   uri = Services.io.newURI(convertToCodeURI(uri.spec));
491   let raw = await fetchFile(uri.spec);
492   let data;
493   try {
494     data = JSON.parse(raw);
495   } catch (ex) {
496     return uri;
497   }
499   // Simplistic test for whether this is a webextension manifest:
500   if (data.manifest_version !== 2) {
501     return uri;
502   }
504   if (data.background?.scripts) {
505     for (let bgscript of data.background.scripts) {
506       gReferencesFromCode.set(uri.resolve(bgscript), null);
507     }
508   }
510   if (data.icons) {
511     for (let icon of Object.values(data.icons)) {
512       gReferencesFromCode.set(uri.resolve(icon), null);
513     }
514   }
516   if (data.experiment_apis) {
517     for (let api of Object.values(data.experiment_apis)) {
518       if (api.parent && api.parent.script) {
519         let script = uri.resolve(api.parent.script);
520         gReferencesFromCode.set(script, null);
521       }
523       if (api.schema) {
524         gReferencesFromCode.set(uri.resolve(api.schema), null);
525       }
526     }
527   }
529   if (data.theme_experiment && data.theme_experiment.stylesheet) {
530     let stylesheet = uri.resolve(data.theme_experiment.stylesheet);
531     gReferencesFromCode.set(stylesheet, null);
532   }
534   for (let themeKey of ["theme", "dark_theme"]) {
535     if (data?.[themeKey]?.images?.additional_backgrounds) {
536       for (let background of data[themeKey].images.additional_backgrounds) {
537         gReferencesFromCode.set(uri.resolve(background), null);
538       }
539     }
540   }
542   return null;
545 function addCodeReference(url, fromURI) {
546   let from = convertToCodeURI(fromURI.spec);
548   // Ignore self references.
549   if (url == from) {
550     return;
551   }
553   let ref;
554   if (gReferencesFromCode.has(url)) {
555     ref = gReferencesFromCode.get(url);
556     if (ref === null) {
557       return;
558     }
559   } else {
560     ref = new Set();
561     gReferencesFromCode.set(url, ref);
562   }
563   ref.add(from);
566 function listCodeReferences(refs) {
567   let refList = [];
568   if (refs) {
569     for (let ref of refs) {
570       refList.push(ref);
571     }
572   }
573   return refList.join(",");
576 function parseCSSFile(fileUri) {
577   return fetchFile(fileUri.spec).then(data => {
578     for (let line of data.split("\n")) {
579       let urls = line.match(/url\([^()]+\)/g);
580       if (!urls) {
581         // @import rules can take a string instead of a url.
582         let importMatch = line.match(/@import ['"]?([^'"]*)['"]?/);
583         if (importMatch && importMatch[1]) {
584           let url = Services.io.newURI(importMatch[1], null, fileUri).spec;
585           addCodeReference(convertToCodeURI(url), fileUri);
586         }
587         continue;
588       }
590       for (let url of urls) {
591         // Remove the url(" prefix and the ") suffix.
592         url = url
593           .replace(/url\(([^)]*)\)/, "$1")
594           .replace(/^"(.*)"$/, "$1")
595           .replace(/^'(.*)'$/, "$1");
596         if (url.startsWith("data:")) {
597           continue;
598         }
600         try {
601           url = Services.io.newURI(url, null, fileUri).specIgnoringRef;
602           addCodeReference(convertToCodeURI(url), fileUri);
603         } catch (e) {
604           ok(false, "unexpected error while resolving this URI: " + url);
605         }
606       }
607     }
608   });
611 function parseCodeFile(fileUri) {
612   return fetchFile(fileUri.spec).then(data => {
613     let baseUri;
614     for (let line of data.split("\n")) {
615       let urls = line.match(
616         /["'`]chrome:\/\/[a-zA-Z0-9-]+\/(content|skin|locale)\/[^"'` ]*["'`]/g
617       );
619       if (!urls) {
620         urls = line.match(/["']resource:\/\/[^"']+["']/g);
621         if (
622           urls &&
623           isDevtools &&
624           /baseURI: "resource:\/\/devtools\//.test(line)
625         ) {
626           baseUri = Services.io.newURI(urls[0].slice(1, -1));
627           continue;
628         }
629       }
631       if (!urls) {
632         urls = line.match(/[a-z0-9_\/-]+\.ftl/i);
633         if (urls) {
634           urls = urls[0];
635           let grePrefix = Services.io.newURI(
636             "resource://gre/localization/en-US/"
637           );
638           let appPrefix = Services.io.newURI(
639             "resource://app/localization/en-US/"
640           );
642           let grePrefixUrl = Services.io.newURI(urls, null, grePrefix).spec;
643           let appPrefixUrl = Services.io.newURI(urls, null, appPrefix).spec;
645           addCodeReference(grePrefixUrl, fileUri);
646           addCodeReference(appPrefixUrl, fileUri);
647           continue;
648         }
649       }
651       if (!urls) {
652         // If there's no absolute chrome URL, look for relative ones in
653         // src and href attributes.
654         let match = line.match("(?:src|href)=[\"']([^$&\"']+)");
655         if (match && match[1]) {
656           let url = Services.io.newURI(match[1], null, fileUri).spec;
657           addCodeReference(convertToCodeURI(url), fileUri);
658         }
660         // This handles `import` lines which may be multi-line.
661         // We have an ESLint rule, `import/no-unassigned-import` which prevents
662         // using bare `import "foo.js"`, so we don't need to handle that case
663         // here.
664         match = line.match(/from\W*['"](.*?)['"]/);
665         if (match?.[1]) {
666           let url = match[1];
667           url = Services.io.newURI(url, null, baseUri || fileUri).spec;
668           url = convertToCodeURI(url);
669           addCodeReference(url, fileUri);
670         }
672         if (isDevtools) {
673           let rules = [
674             ["devtools/client/locales", "chrome://devtools/locale"],
675             ["devtools/shared/locales", "chrome://devtools-shared/locale"],
676             [
677               "devtools/shared/platform",
678               "resource://devtools/shared/platform/chrome",
679             ],
680             ["devtools", "resource://devtools"],
681           ];
683           match = line.match(/["']((?:devtools)\/[^\\#"']+)["']/);
684           if (match && match[1]) {
685             let path = match[1];
686             for (let rule of rules) {
687               if (path.startsWith(rule[0] + "/")) {
688                 path = path.replace(rule[0], rule[1]);
689                 if (!/\.(properties|js|jsm|mjs|json|css)$/.test(path)) {
690                   path += ".js";
691                 }
692                 addCodeReference(path, fileUri);
693                 break;
694               }
695             }
696           }
698           match = line.match(/require\(['"](\.[^'"]+)['"]\)/);
699           if (match && match[1]) {
700             let url = match[1];
701             url = Services.io.newURI(url, null, baseUri || fileUri).spec;
702             url = convertToCodeURI(url);
703             if (!/\.(properties|js|jsm|mjs|json|css)$/.test(url)) {
704               url += ".js";
705             }
706             if (url.startsWith("resource://")) {
707               addCodeReference(url, fileUri);
708             } else {
709               // if we end up with a chrome:// url here, it's likely because
710               // a baseURI to a resource:// path has been defined in another
711               // .js file that is loaded in the same scope, we can't detect it.
712             }
713           }
714         }
715         continue;
716       }
718       for (let url of urls) {
719         // Remove quotes.
720         url = url.slice(1, -1);
721         // Remove ? or \ trailing characters.
722         if (url.endsWith("\\")) {
723           url = url.slice(0, -1);
724         }
726         let pos = url.indexOf("?");
727         if (pos != -1) {
728           url = url.slice(0, pos);
729         }
731         // Make urls like chrome://browser/skin/ point to an actual file,
732         // and remove the ref if any.
733         try {
734           url = Services.io.newURI(url).specIgnoringRef;
735         } catch (e) {
736           continue;
737         }
739         if (
740           isDevtools &&
741           line.includes("require(") &&
742           !/\.(properties|js|jsm|mjs|json|css)$/.test(url)
743         ) {
744           url += ".js";
745         }
747         addCodeReference(url, fileUri);
748       }
749     }
750   });
753 function convertToCodeURI(fileUri) {
754   let baseUri = fileUri;
755   let path = "";
756   while (true) {
757     let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
758     if (slashPos <= 0) {
759       // File not accessible from chrome protocol, try resource://
760       for (let res of gResourceMap) {
761         if (fileUri.startsWith(res[1])) {
762           return fileUri.replace(res[1], "resource://" + res[0] + "/");
763         }
764       }
765       // Give up and return the original URL.
766       return fileUri;
767     }
768     path = baseUri.slice(slashPos + 1) + path;
769     baseUri = baseUri.slice(0, slashPos + 1);
770     if (gChromeMap.has(baseUri)) {
771       return gChromeMap.get(baseUri) + path;
772     }
773   }
776 async function chromeFileExists(aURI) {
777   try {
778     return await PerfTestHelpers.checkURIExists(aURI);
779   } catch (e) {
780     todo(false, `Failed to check if ${aURI} exists: ${e}`);
781     return false;
782   }
785 function findChromeUrlsFromArray(array, prefix) {
786   // Find the first character of the prefix...
787   for (
788     let index = 0;
789     (index = array.indexOf(prefix.charCodeAt(0), index)) != -1;
790     ++index
791   ) {
792     // Then ensure we actually have the whole prefix.
793     let found = true;
794     for (let i = 1; i < prefix.length; ++i) {
795       if (array[index + i] != prefix.charCodeAt(i)) {
796         found = false;
797         break;
798       }
799     }
800     if (!found) {
801       continue;
802     }
804     // C strings are null terminated, but " also terminates urls
805     // (nsIndexedToHTML.cpp contains an HTML fragment with several chrome urls)
806     // Let's also terminate the string on the # character to skip references.
807     let end = Math.min(
808       array.indexOf(0, index),
809       array.indexOf('"'.charCodeAt(0), index),
810       array.indexOf(")".charCodeAt(0), index),
811       array.indexOf("#".charCodeAt(0), index)
812     );
813     let string = "";
814     for (; index < end; ++index) {
815       string += String.fromCharCode(array[index]);
816     }
818     // Only keep strings that look like real chrome or resource urls.
819     if (
820       /chrome:\/\/[a-zA-Z09-]+\/(content|skin|locale)\//.test(string) ||
821       /resource:\/\/[a-zA-Z09-]*\/.*\.[a-z]+/.test(string)
822     ) {
823       gReferencesFromCode.set(string, null);
824     }
825   }
828 add_task(async function checkAllTheFiles() {
829   TestUtils.assertPackagedBuild();
831   const libxul = await IOUtils.read(PathUtils.xulLibraryPath);
832   findChromeUrlsFromArray(libxul, "chrome://");
833   findChromeUrlsFromArray(libxul, "resource://");
834   // Handle NS_LITERAL_STRING.
835   let uint16 = new Uint16Array(libxul.buffer);
836   findChromeUrlsFromArray(uint16, "chrome://");
837   findChromeUrlsFromArray(uint16, "resource://");
839   const kCodeExtensions = [
840     ".xml",
841     ".xsl",
842     ".mjs",
843     ".js",
844     ".jsm",
845     ".json",
846     ".html",
847     ".xhtml",
848   ];
850   let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
851   // This asynchronously produces a list of URLs (sadly, mostly sync on our
852   // test infrastructure because it runs against jarfiles there, and
853   // our zipreader APIs are all sync)
854   let uris = await generateURIsFromDirTree(
855     appDir,
856     [
857       ".css",
858       ".manifest",
859       ".jpg",
860       ".png",
861       ".gif",
862       ".svg",
863       ".ftl",
864       ".dtd",
865       ".properties",
866     ].concat(kCodeExtensions)
867   );
869   // Parse and remove all manifests from the list.
870   // NOTE that this must be done before filtering out devtools paths
871   // so that all chrome paths can be recorded.
872   let manifestURIs = [];
873   let jsonManifests = [];
874   uris = uris.filter(uri => {
875     let path = uri.pathQueryRef;
876     if (path.endsWith(".manifest")) {
877       manifestURIs.push(uri);
878       return false;
879     } else if (path.endsWith("/manifest.json")) {
880       jsonManifests.push(uri);
881       return false;
882     }
884     return true;
885   });
887   // Wait for all manifest to be parsed
888   await PerfTestHelpers.throttledMapPromises(manifestURIs, parseManifest);
890   for (let esModule of Components.manager.getComponentESModules()) {
891     gReferencesFromCode.set(esModule, null);
892   }
894   // manifest.json is a common name, it is used for WebExtension manifests
895   // but also for other things.  To tell them apart, we have to actually
896   // read the contents.  This will populate gExtensionRoots with all
897   // embedded extension APIs, and return any manifest.json files that aren't
898   // webextensions.
899   let nonWebextManifests = (
900     await Promise.all(jsonManifests.map(parseJsonManifest))
901   ).filter(uri => !!uri);
902   uris.push(...nonWebextManifests);
904   // We build a list of promises that get resolved when their respective
905   // files have loaded and produced no errors.
906   let allPromises = [];
908   for (let uri of uris) {
909     let path = uri.pathQueryRef;
910     if (path.endsWith(".css")) {
911       allPromises.push([parseCSSFile, uri]);
912     } else if (kCodeExtensions.some(ext => path.endsWith(ext))) {
913       allPromises.push([parseCodeFile, uri]);
914     }
915   }
917   // Wait for all the files to have actually loaded:
918   await PerfTestHelpers.throttledMapPromises(allPromises, ([task, uri]) =>
919     task(uri)
920   );
922   // Keep only chrome:// files, and filter out either the devtools paths or
923   // the non-devtools paths:
924   let devtoolsPrefixes = [
925     "chrome://devtools",
926     "resource://devtools/",
927     "resource://devtools-shared-images/",
928     "resource://devtools-highlighter-styles/",
929     "resource://app/modules/devtools",
930     "resource://gre/modules/devtools",
931     "resource://app/localization/en-US/startup/aboutDevTools.ftl",
932     "resource://app/localization/en-US/devtools/",
933   ];
934   let hasDevtoolsPrefix = uri =>
935     devtoolsPrefixes.some(prefix => uri.startsWith(prefix));
936   let chromeFiles = [];
937   for (let uri of uris) {
938     uri = convertToCodeURI(uri.spec);
939     if (
940       (uri.startsWith("chrome://") || uri.startsWith("resource://")) &&
941       isDevtools == hasDevtoolsPrefix(uri)
942     ) {
943       chromeFiles.push(uri);
944     }
945   }
947   if (isDevtools) {
948     // chrome://devtools/skin/devtools-browser.css is included from browser.xhtml
949     gReferencesFromCode.set(AppConstants.BROWSER_CHROME_URL, null);
950     // devtools' css is currently included from browser.css, see bug 1204810.
951     gReferencesFromCode.set("chrome://browser/skin/browser.css", null);
952   }
954   let isUnreferenced = file => {
955     if (gExceptionPaths.some(e => file.startsWith(e))) {
956       return false;
957     }
958     if (gReferencesFromCode.has(file)) {
959       let refs = gReferencesFromCode.get(file);
960       if (refs === null) {
961         return false;
962       }
963       for (let ref of refs) {
964         if (isDevtools) {
965           if (
966             ref.startsWith("resource://app/components/") ||
967             (file.startsWith("chrome://") && ref.startsWith("resource://"))
968           ) {
969             return false;
970           }
971         }
973         if (gReferencesFromCode.has(ref)) {
974           let refType = gReferencesFromCode.get(ref);
975           if (
976             refType === null || // unconditionally referenced
977             refType == "allowlist" ||
978             refType == "allowlist-direct"
979           ) {
980             return false;
981           }
982         }
983       }
984     }
985     return !gOverrideMap.has(file) || isUnreferenced(gOverrideMap.get(file));
986   };
988   let unreferencedFiles = chromeFiles;
990   let removeReferenced = useAllowlist => {
991     let foundReference = false;
992     unreferencedFiles = unreferencedFiles.filter(f => {
993       let rv = isUnreferenced(f);
994       if (rv && f.startsWith("resource://app/")) {
995         rv = isUnreferenced(f.replace("resource://app/", "resource:///"));
996       }
997       if (rv && /^resource:\/\/(?:app|gre)\/components\/[^/]+\.js$/.test(f)) {
998         rv = !gComponentsSet.has(f.replace(/.*\//, ""));
999       }
1000       if (!rv) {
1001         foundReference = true;
1002         if (useAllowlist) {
1003           info(
1004             "indirectly allowlisted file: " +
1005               f +
1006               " used from " +
1007               listCodeReferences(gReferencesFromCode.get(f))
1008           );
1009         }
1010         gReferencesFromCode.set(f, useAllowlist ? "allowlist" : null);
1011       }
1012       return rv;
1013     });
1014     return foundReference;
1015   };
1016   // First filter out the files that are referenced.
1017   while (removeReferenced(false)) {
1018     // As long as removeReferenced returns true, some files have been marked
1019     // as referenced, so we need to run it again.
1020   }
1021   // Marked as referenced the files that have been explicitly allowed.
1022   unreferencedFiles = unreferencedFiles.filter(file => {
1023     if (allowlist.has(file)) {
1024       allowlist.delete(file);
1025       gReferencesFromCode.set(file, "allowlist-direct");
1026       return false;
1027     }
1028     return true;
1029   });
1030   // Run the process again, this time when more files are marked as referenced,
1031   // it's a consequence of the allowlist.
1032   while (removeReferenced(true)) {
1033     // As long as removeReferenced returns true, we need to run it again.
1034   }
1036   unreferencedFiles.sort();
1038   if (isDevtools) {
1039     // Bug 1351878 - handle devtools resource files
1040     unreferencedFiles = unreferencedFiles.filter(file => {
1041       if (file.startsWith("resource://")) {
1042         info("unreferenced devtools resource file: " + file);
1043         return false;
1044       }
1045       return true;
1046     });
1047   }
1049   is(unreferencedFiles.length, 0, "there should be no unreferenced files");
1050   for (let file of unreferencedFiles) {
1051     let refs = gReferencesFromCode.get(file);
1052     if (refs === undefined) {
1053       ok(false, "unreferenced file: " + file);
1054     } else {
1055       let refList = listCodeReferences(refs);
1056       let msg = "file only referenced from unreferenced files: " + file;
1057       if (refList) {
1058         msg += " referenced from " + refList;
1059       }
1060       ok(false, msg);
1061     }
1062   }
1064   for (let file of allowlist) {
1065     if (ignorableAllowlist.has(file)) {
1066       info("ignored unused allowlist entry: " + file);
1067     } else {
1068       ok(false, "unused allowlist entry: " + file);
1069     }
1070   }
1072   for (let [file, refs] of gReferencesFromCode) {
1073     if (
1074       isDevtools != devtoolsPrefixes.some(prefix => file.startsWith(prefix))
1075     ) {
1076       continue;
1077     }
1079     if (
1080       (file.startsWith("chrome://") || file.startsWith("resource://")) &&
1081       !(await chromeFileExists(file))
1082     ) {
1083       // Ignore chrome prefixes that have been automatically expanded.
1084       let pathParts =
1085         file.match("chrome://([^/]+)/content/([^/.]+).xul") ||
1086         file.match("chrome://([^/]+)/skin/([^/.]+).css");
1087       if (pathParts && pathParts[1] == pathParts[2]) {
1088         continue;
1089       }
1091       // TODO: bug 1349010 - add a allowlist and make this reliable enough
1092       // that we could make the test fail when this catches something new.
1093       let refList = listCodeReferences(refs);
1094       let msg = "missing file: " + file;
1095       if (refList) {
1096         msg += " referenced from " + refList;
1097       }
1098       info(msg);
1099     }
1100   }