Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(w3c/linter-rules/required-sections): required sections linter #3922

Merged
merged 19 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion profiles/w3c.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const modules = [
import("../src/core/linter-rules/local-refs-exist.js"),
import("../src/core/linter-rules/no-headingless-sections.js"),
import("../src/core/linter-rules/no-unused-vars.js"),
import("../src/core/linter-rules/privsec-section.js"),
import("../src/w3c/linter-rules/required-sections.js"),
import("../src/core/linter-rules/wpt-tests-exist.js"),
import("../src/core/linter-rules/no-http-props.js"),
import("../src/core/linter-rules/a11y.js"),
Expand Down
2 changes: 1 addition & 1 deletion src/w3c/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const w3cLogo = {

const w3cDefaults = {
lint: {
"privsec-section": true,
"required-sections": true,
"wpt-tests-exist": false,
a11y: false,
},
Expand Down
2 changes: 1 addition & 1 deletion src/w3c/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export const registryTrackStatus = ["DRY", "CRY", "CRYD", "RY"];
export const cgStatus = ["CG-DRAFT", "CG-FINAL"];
export const bgStatus = ["BG-DRAFT", "BG-FINAL"];
export const cgbgStatus = [...cgStatus, ...bgStatus];
const noTrackStatus = [
export const noTrackStatus = [
"base",
...cgStatus,
...bgStatus,
Expand Down
76 changes: 76 additions & 0 deletions src/w3c/linter-rules/required-sections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// @ts-check
/**
* The W3C Process requires separate Privacy Considerations and Security
* Considerations sections. This linter checks for the presence of these
* sections, and reports an error if they are not present.
*/

import {
InsensitiveStringSet,
docLink,
getIntlData,
norm,
showError,
showWarning,
} from "../../core/utils.js";
import { recTrackStatus } from "../headers.js";

const ruleName = "required-sections";
export const name = "w3c/linter-rules/required-sections";

const localizationStrings = {
en: {
msg(sectionTitle) {
return `W3C Recommendation track documents require a separate "${sectionTitle}" section.`;
},
hint(sectionTitle) {
return docLink`Add a \`<section>\` with a "${sectionTitle}" header. See the [Horizontal review guidelines](https://www.w3.org/Guide/documentreview/#how_to_get_horizontal_review).
If the document is not intended for the W3C Recommendation track, set ${"[noRecTrack]"} to \`true\`
or turn off the ${`[${ruleName}]`} linter rule.`;
},
},
};
const l10n = getIntlData(localizationStrings);

export const requiresSomeSectionStatus = new Set([...recTrackStatus, "ED"]);
requiresSomeSectionStatus.delete("DISC"); // "Discontinued Draft"

export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}

if (conf.noRecTrack || !requiresSomeSectionStatus.has(conf.specStatus)) {
return;
}

const logger = conf.lint[ruleName] === "error" ? showError : showWarning;

const missingRequiredSections = new InsensitiveStringSet([
"Privacy Considerations",
"Security Considerations",
]);

/** @type {NodeListOf<HTMLElement>} */
const headers = document.querySelectorAll("h2, h3, h4, h5, h6");
for (const header of headers) {
const clone = header.cloneNode(true);
// section number and self-link anchor
clone.querySelectorAll("bdi, .self-link")?.forEach(elem => elem.remove());
const text = norm(clone.textContent);
if (missingRequiredSections.has(text)) {
missingRequiredSections.delete(text);
// Check if we find them all...
if (missingRequiredSections.size === 0) {
return; // All present, early return!
}
}
}

// Show the ones we didn't find individually
for (const title of missingRequiredSections) {
logger(l10n.msg(title), name, {
hint: l10n.hint(title),
});
}
}
158 changes: 158 additions & 0 deletions tests/spec/w3c/linter-rules/required-sections-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use strict";

import {
errorFilters,
flushIframes,
makeRSDoc,
makeStandardOps,
warningFilters,
} from "../../SpecHelper.js";
import { noTrackStatus } from "../../../../src/w3c/headers.js";
import { requiresSomeSectionStatus } from "../../../../src/w3c/linter-rules/required-sections.js";

describe("w3c — required-sections", () => {
afterAll(() => {
flushIframes();
});

const errorsFilter = errorFilters.filter(
"w3c/linter-rules/required-sections"
);
const warningsFilter = warningFilters.filter(
"w3c/linter-rules/required-sections"
);

it("does nothing if disabled", async () => {
const ops = makeStandardOps({ lint: { "required-sections": false } });
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).toHaveSize(0);
expect(warnings).toHaveSize(0);
});

it("allows using 'error' as the logger", async () => {
const ops = makeStandardOps({ lint: { "required-sections": "error" } });
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).toHaveSize(2);
expect(warnings).toHaveSize(0);
});

it("allows using 'warn' as the logger", async () => {
const ops = makeStandardOps({ lint: { "required-sections": "warn" } });
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).toHaveSize(0);
expect(warnings).toHaveSize(2);
});

it("doesn't lint non-rec-track docs", async () => {
for (const specStatus of noTrackStatus) {
const ops = makeStandardOps({
lint: { "required-sections": true },
specStatus,
});
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).withContext(specStatus).toHaveSize(0);
expect(warnings).withContext(specStatus).toHaveSize(0);
}
});

it("doesn't lint for when explicitly marked as noRecTrack", async () => {
const ops = makeStandardOps({
lint: { "required-sections": true },
noRecTrack: true,
specStatus: "ED",
});
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).toHaveSize(0);
expect(warnings).toHaveSize(0);
});

it("generates warning by default when its a rec track document and both privacy and security sections are missing", async () => {
for (const specStatus of requiresSomeSectionStatus) {
const ops = makeStandardOps({
lint: { "required-sections": true },
specStatus,
});
const doc = await makeRSDoc(ops);
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(errors).withContext(specStatus).toHaveSize(0);
expect(warnings).withContext(specStatus).toHaveSize(2);
}
});

it("generates an error if privacy section if present, but security is missing", async () => {
const body = `
<section>
<h2>Privacy considerations</h2>
<p>This is a privacy section</p>
</section>
`;
const conf = {
lint: { "required-sections": "error" },
specStatus: "WD",
};
const doc = await makeRSDoc(makeStandardOps(conf, body));
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(warnings).toHaveSize(0);
expect(errors).toHaveSize(1);
const [error] = errors;
expect(error.message).toContain(
'separate "Security Considerations" section'
);
});

it("generates an error if Security section if present, but privacy is missing", async () => {
const body = `
<section>
<h2>Security considerations</h2>
<p>This is a security section</p>
</section>
`;
const conf = {
lint: { "required-sections": "error" },
specStatus: "WD",
};
const doc = await makeRSDoc(makeStandardOps(conf, body));
const errors = errorsFilter(doc);
const warnings = warningsFilter(doc);
expect(warnings).toHaveSize(0);
expect(errors).toHaveSize(1);
const [error] = errors;
expect(error.message).toContain(
'separate "Privacy Considerations" section'
);
});

it("generates no errors when both the Privacy and Security considerations sections are present", async () => {
const body = `
<section>
<h2>Privacy Considerations</h2>
<p>This is a privacy section</p>
</section>
<section>
<h2>Security Considerations</h2>
<p>This is a security section</p>
</section>
`;
const conf = {
lint: { "required-sections": true },
specStatus: "WD",
};
const doc = await makeRSDoc(makeStandardOps(conf, body));
const errors = errorsFilter(doc);
expect(errors).toHaveSize(0);
const warnings = warningsFilter(doc);
expect(warnings).toHaveSize(0);
});
});