Skip to content

Commit be52a91

Browse files
authored
feat(linter-rules/no-unused-vars): declared var must be used (#2955)
1 parent 2bcffb6 commit be52a91

File tree

5 files changed

+175
-0
lines changed

5 files changed

+175
-0
lines changed

src/core/defaults.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import linter from "./linter.js";
1010
import { rule as localRefsExist } from "./linter-rules/local-refs-exist.js";
1111
import { rule as noHeadinglessSectionsRule } from "./linter-rules/no-headingless-sections.js";
1212
import { rule as noHttpPropsRule } from "./linter-rules/no-http-props.js";
13+
import { rule as noUnusedVars } from "./linter-rules/no-unused-vars.js";
1314
import { rule as privsecSection } from "./linter-rules/privsec-section.js";
1415

1516
linter.register(
1617
noHttpPropsRule,
1718
noHeadinglessSectionsRule,
19+
noUnusedVars,
1820
checkPunctuation,
1921
localRefsExist,
2022
checkInternalSlots,
@@ -26,6 +28,7 @@ export const coreDefaults = {
2628
lint: {
2729
"no-headingless-sections": true,
2830
"no-http-props": true,
31+
"no-unused-vars": false,
2932
"check-punctuation": false,
3033
"local-refs-exist": true,
3134
"check-internal-slots": false,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// @ts-check
2+
/**
3+
* Linter rule "no-unused-vars".
4+
*
5+
* Checks that an variable is used if declared (the first use is treated as
6+
* declaration).
7+
*/
8+
import LinterRule from "../LinterRule.js";
9+
import { lang as defaultLang } from "../l10n.js";
10+
import { norm } from "../utils.js";
11+
12+
const name = "no-unused-vars";
13+
14+
const meta = {
15+
en: {
16+
description: "Variable was defined, but never used.",
17+
howToFix: "Add a `data-ignore-unused` attribute to the `<var>`.",
18+
help: "See developer console.",
19+
},
20+
};
21+
// Fall back to english, if language is missing
22+
const lang = defaultLang in meta ? defaultLang : "en";
23+
24+
/**
25+
* @param {*} _
26+
* @param {Document} doc
27+
* @return {import("../LinterRule").LinterResult}
28+
*/
29+
function linterFunction(_, doc) {
30+
const offendingElements = [];
31+
32+
/**
33+
* Check if a <section> contains a `".algorithm"`
34+
*
35+
* The selector matches:
36+
* ``` html
37+
* <section><ul class="algorithm"></ul></section>
38+
* <section><div><ul class="algorithm"></ul></div></section>
39+
* ```
40+
* The selector does not match:
41+
* ``` html
42+
* <section><section><ul class="algorithm"></ul></section></section>
43+
* ```
44+
* @param {HTMLElement} section
45+
*/
46+
const sectionContainsAlgorithm = section =>
47+
!!section.querySelector(
48+
":scope > :not(section) ~ .algorithm, :scope > :not(section) .algorithm"
49+
);
50+
51+
for (const section of doc.querySelectorAll("section")) {
52+
if (!sectionContainsAlgorithm(section)) continue;
53+
54+
/**
55+
* `<var>` in this section, but excluding those in child sections.
56+
* @type {NodeListOf<HTMLElement>}
57+
*/
58+
const varElems = section.querySelectorAll(":scope > :not(section) var");
59+
if (!varElems.length) continue;
60+
61+
/** @type {Map<string, HTMLElement[]>} */
62+
const varUsage = new Map();
63+
for (const varElem of varElems) {
64+
const key = norm(varElem.textContent);
65+
const elems = varUsage.get(key) || varUsage.set(key, []).get(key);
66+
elems.push(varElem);
67+
}
68+
69+
for (const vars of varUsage.values()) {
70+
if (vars.length === 1 && !vars[0].hasAttribute("data-ignore-unused")) {
71+
offendingElements.push(vars[0]);
72+
}
73+
}
74+
}
75+
76+
if (!offendingElements.length) {
77+
return;
78+
}
79+
return {
80+
name,
81+
offendingElements,
82+
occurrences: offendingElements.length,
83+
...meta[lang],
84+
};
85+
}
86+
export const rule = new LinterRule(name, linterFunction);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use strict";
2+
3+
import { rule } from "../../../../src/core/linter-rules/no-unused-vars.js";
4+
5+
describe("Core Linter Rule - 'no-unused-vars'", () => {
6+
const config = {
7+
lint: { "no-unused-vars": true },
8+
};
9+
const doc = document.implementation.createHTMLDocument("test doc");
10+
beforeEach(() => {
11+
// Make sure every unordered test get an empty document
12+
// See: https://github.com/w3c/respec/pull/1495
13+
while (doc.body.firstChild) {
14+
doc.body.removeChild(doc.body.firstChild);
15+
}
16+
});
17+
18+
it("skips sections without .algorithm", async () => {
19+
doc.body.innerHTML = `
20+
<section>
21+
<p><var>varA</var></p>
22+
<section>
23+
<p><var>varC</var></p>
24+
</section>
25+
</section>
26+
`;
27+
28+
const result = await rule.lint(config, doc);
29+
expect(result).toBeUndefined();
30+
});
31+
32+
it("complains on unused vars", async () => {
33+
doc.body.innerHTML = `
34+
<section>
35+
<p><var>A</var> is unused</p>
36+
<p><var>B</var></p>
37+
<p><var>Z</var> is unused</p>
38+
<ul class="algorithm">
39+
<li><var>B</var></li>
40+
<li><var>C</var> is unused</li>
41+
<li><var>D</var></li>
42+
<li><var>D</var></li>
43+
</ul>
44+
<section>
45+
<p><var>E</var> is unused</p>
46+
<ul class="algorithm">
47+
<li><var>F</var> is unused</li>
48+
<li><var>G</var></li>
49+
<li><var>G</var></li>
50+
<li><var>Z</var> is unused even though it's used in grandparent</li>
51+
</ul>
52+
</section>
53+
</section>
54+
`;
55+
56+
const result = await rule.lint(config, doc);
57+
expect(result.name).toBe("no-unused-vars");
58+
const unusedVars = result.offendingElements.map(v => v.textContent);
59+
expect(unusedVars).toEqual(["A", "Z", "C", "E", "F", "Z"]);
60+
expect(result.occurrences).toBe(6);
61+
});
62+
63+
it("ignores unused vars with data-ignore-unused", async () => {
64+
doc.body.innerHTML = `
65+
<section>
66+
<p><var data-ignore-unused>varA</var> is unused</p>
67+
<ul class="algorithm">
68+
<li><var>varB</var> is unused</li>
69+
<li><var data-ignore-unused>varC</var> is unused</li>
70+
<li><var>varD</var></li>
71+
<li><var>varD</var></li>
72+
</ul>
73+
</section>
74+
`;
75+
76+
const result = await rule.lint(config, doc);
77+
expect(result.name).toBe("no-unused-vars");
78+
expect(result.occurrences).toBe(1);
79+
const unusedVars = result.offendingElements.map(v => v.textContent);
80+
expect(unusedVars).toEqual(["varB"]);
81+
});
82+
});

tests/spec/geonovum/defaults-spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe("Geonovum — Defaults", () => {
1616
"no-headingless-sections": true,
1717
"privsec-section": true,
1818
"no-http-props": true,
19+
"no-unused-vars": false,
1920
"local-refs-exist": true,
2021
"check-punctuation": false,
2122
"check-internal-slots": false,
@@ -53,6 +54,7 @@ describe("Geonovum — Defaults", () => {
5354
"no-headingless-sections": true,
5455
"privsec-section": false,
5556
"no-http-props": false,
57+
"no-unused-vars": false,
5658
"local-refs-exist": true,
5759
"check-punctuation": false,
5860
"fake-linter-rule": "foo",

tests/spec/w3c/defaults-spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe("W3C — Defaults", () => {
1515
"no-headingless-sections": true,
1616
"privsec-section": true,
1717
"no-http-props": true,
18+
"no-unused-vars": false,
1819
"local-refs-exist": true,
1920
"check-punctuation": false,
2021
"check-internal-slots": false,
@@ -53,6 +54,7 @@ describe("W3C — Defaults", () => {
5354
"no-headingless-sections": true,
5455
"privsec-section": false,
5556
"no-http-props": false,
57+
"no-unused-vars": false,
5658
"local-refs-exist": true,
5759
"check-punctuation": false,
5860
"fake-linter-rule": "foo",

0 commit comments

Comments
 (0)