Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 186d63f

Browse files
committed
Bug 1479515 - Add an ESLint rule to check for valid property accesses on Services. r=Gijs
This is manually run and requires a full build. MOZ_OBJDIR needs to be set to point to the object directory. Differential Revision: https://phabricator.services.mozilla.com/D156426
1 parent 274f866 commit 186d63f

7 files changed

Lines changed: 204 additions & 2 deletions

File tree

devtools/client/shared/test-helpers/jest-fixtures/Services.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ const Services = {
552552
};
553553

554554
function pref(name, value) {
555+
// eslint-disable-next-line mozilla/valid-services-property
555556
const thePref = Services.prefs._findOrCreatePref(name, value, true, value);
556557
thePref._setDefault(value);
557558
}

docs/code-quality/lint/linters/eslint-plugin-mozilla.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ The plugin implements the following rules:
7474
eslint-plugin-mozilla/use-services
7575
eslint-plugin-mozilla/valid-ci-uses
7676
eslint-plugin-mozilla/valid-lazy
77+
eslint-plugin-mozilla/valid-services
78+
eslint-plugin-mozilla/valid-services-property
7779
eslint-plugin-mozilla/var-only-at-top-level
7880

7981
Tests
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
valid-services-property
2+
=======================
3+
4+
Ensures that accesses of properties of items accessed via the ``Services``
5+
object are valid.
6+
7+
This rule requires a full build to run, and is not turned on by default. To run
8+
this rule manually, use:
9+
10+
.. code-block:: console
11+
12+
MOZ_OBJDIR=objdir-ff-opt ./mach eslint --rule="mozilla/valid-services-property: error" *
13+
14+
Examples of incorrect code for this rule:
15+
-----------------------------------------
16+
17+
Assuming ``foo`` is not defined within ``Ci.nsISearchService``.
18+
19+
.. code-block:: js
20+
21+
Services.search.foo();
22+
23+
Examples of correct code for this rule:
24+
---------------------------------------
25+
26+
Assuming ``bar`` is defined within ``Ci.nsISearchService``.
27+
28+
.. code-block:: js
29+
30+
Services.search.bar();

tools/lint/eslint/eslint-plugin-mozilla/lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ module.exports = {
8585
"valid-ci-uses": require("../lib/rules/valid-ci-uses"),
8686
"valid-lazy": require("../lib/rules/valid-lazy"),
8787
"valid-services": require("../lib/rules/valid-services"),
88+
"valid-services-property": require("../lib/rules/valid-services-property"),
8889
"var-only-at-top-level": require("../lib/rules/var-only-at-top-level"),
8990
},
9091
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @fileoverview Ensures that property accesses on Services.<alias> are valid.
3+
* Although this largely duplicates the valid-services rule, the checks here
4+
* require an objdir and a manual run.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public
7+
* License, v. 2.0. If a copy of the MPL was not distributed with this
8+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
9+
*/
10+
11+
"use strict";
12+
13+
const helpers = require("../helpers");
14+
15+
function findInterfaceNames(name) {
16+
let interfaces = [];
17+
for (let [key, value] of Object.entries(helpers.servicesData)) {
18+
if (value == name) {
19+
interfaces.push(key);
20+
}
21+
}
22+
return interfaces;
23+
}
24+
25+
function isInInterface(interfaceName, name) {
26+
let interfaceDetails = helpers.xpidlData.get(interfaceName);
27+
28+
// TODO: Bug 1790261 - check only methods if the expression is callable.
29+
if (interfaceDetails.methods.some(m => m.name == name)) {
30+
return true;
31+
}
32+
33+
if (interfaceDetails.consts.some(c => c.name == name)) {
34+
return true;
35+
}
36+
37+
if (interfaceDetails.parent) {
38+
return isInInterface(interfaceDetails.parent, name);
39+
}
40+
return false;
41+
}
42+
43+
module.exports = {
44+
meta: {
45+
docs: {
46+
url:
47+
"https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/valid-services-property.html",
48+
},
49+
messages: {
50+
unknownProperty:
51+
"Unknown property access Services.{{ alias }}.{{ propertyName }}, Interfaces: {{ checkedInterfaces }}",
52+
},
53+
type: "problem",
54+
},
55+
56+
create(context) {
57+
let servicesInterfaceMap = helpers.servicesData;
58+
let serviceAliases = new Set([
59+
...Object.values(servicesInterfaceMap),
60+
// This is defined only for Android, so most builds won't pick it up.
61+
"androidBridge",
62+
// These are defined without interfaces and hence are not in the services map.
63+
"cpmm",
64+
"crashmanager",
65+
"mm",
66+
"ppmm",
67+
// The new xulStore also does not have an interface.
68+
"xulStore",
69+
]);
70+
return {
71+
MemberExpression(node) {
72+
if (node.computed || node.object.type !== "Identifier") {
73+
return;
74+
}
75+
76+
let mainNode;
77+
if (node.object.name == "Services") {
78+
mainNode = node;
79+
} else if (
80+
node.property.name == "Services" &&
81+
node.parent.type == "MemberExpression"
82+
) {
83+
mainNode = node.parent;
84+
} else {
85+
return;
86+
}
87+
88+
let alias = mainNode.property.name;
89+
if (!serviceAliases.has(alias)) {
90+
return;
91+
}
92+
93+
if (
94+
mainNode.parent.type == "MemberExpression" &&
95+
!mainNode.parent.computed
96+
) {
97+
let propertyName = mainNode.parent.property.name;
98+
if (propertyName == "wrappedJSObject") {
99+
return;
100+
}
101+
let interfaces = findInterfaceNames(alias);
102+
if (!interfaces.length) {
103+
return;
104+
}
105+
106+
let checkedInterfaces = [];
107+
for (let item of interfaces) {
108+
if (isInInterface(item, propertyName)) {
109+
return;
110+
}
111+
checkedInterfaces.push(item);
112+
}
113+
context.report({
114+
node,
115+
messageId: "unknownProperty",
116+
data: {
117+
alias,
118+
propertyName,
119+
checkedInterfaces,
120+
},
121+
});
122+
}
123+
},
124+
};
125+
},
126+
};

tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @fileoverview Require use of Services.* rather than getService.
2+
* @fileoverview Ensures that Services uses have valid property names.
33
*
44
* This Source Code Form is subject to the terms of the Mozilla Public
55
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,7 +13,7 @@ module.exports = {
1313
meta: {
1414
docs: {
1515
url:
16-
"https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/use-services.html",
16+
"https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/valid-services.html",
1717
},
1818
type: "problem",
1919
},
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* Any copyright is dedicated to the Public Domain.
2+
* http://creativecommons.org/publicdomain/zero/1.0/ */
3+
4+
"use strict";
5+
6+
// ------------------------------------------------------------------------------
7+
// Requirements
8+
// ------------------------------------------------------------------------------
9+
10+
var rule = require("../lib/rules/valid-services-property");
11+
var RuleTester = require("eslint").RuleTester;
12+
13+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 13 } });
14+
15+
// ------------------------------------------------------------------------------
16+
// Tests
17+
// ------------------------------------------------------------------------------
18+
19+
function invalidCode(code, messageId, data) {
20+
return { code, errors: [{ messageId, data }] };
21+
}
22+
23+
process.env.TEST_XPIDLDIR = `${__dirname}/xpidl`;
24+
25+
ruleTester.run("valid-services-property", rule, {
26+
valid: [
27+
"Services.uriFixup.keywordToURI()",
28+
"Services.uriFixup.FIXUP_FLAG_NONE",
29+
],
30+
invalid: [
31+
invalidCode("Services.uriFixup.UNKNOWN_CONSTANT", "unknownProperty", {
32+
alias: "uriFixup",
33+
propertyName: "UNKNOWN_CONSTANT",
34+
checkedInterfaces: ["nsIURIFixup"],
35+
}),
36+
invalidCode("Services.uriFixup.foo()", "unknownProperty", {
37+
alias: "uriFixup",
38+
propertyName: "foo",
39+
checkedInterfaces: ["nsIURIFixup"],
40+
}),
41+
],
42+
});

0 commit comments

Comments
 (0)