diff --git a/profiles/w3c.js b/profiles/w3c.js index 7b338387d5..62bf5f6443 100644 --- a/profiles/w3c.js +++ b/profiles/w3c.js @@ -68,7 +68,7 @@ const modules = [ import("../src/core/linter-rules/no-unused-dfns.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"), diff --git a/src/w3c/defaults.js b/src/w3c/defaults.js index 3590c8d0de..bbe7a06c73 100644 --- a/src/w3c/defaults.js +++ b/src/w3c/defaults.js @@ -24,7 +24,8 @@ const w3cLogo = { const w3cDefaults = { lint: { - "privsec-section": true, + "privsec-section": false, + "required-sections": true, "wpt-tests-exist": false, "no-unused-dfns": "warn", a11y: false, diff --git a/src/w3c/linter-rules/required-sections.js b/src/w3c/linter-rules/required-sections.js new file mode 100644 index 0000000000..ba98102f2b --- /dev/null +++ b/src/w3c/linter-rules/required-sections.js @@ -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 \`
\` 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} */ + 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), + }); + } +} diff --git a/tests/spec/core/respec-global-spec.js b/tests/spec/core/respec-global-spec.js index 2797457f53..a8ab4254a0 100644 --- a/tests/spec/core/respec-global-spec.js +++ b/tests/spec/core/respec-global-spec.js @@ -22,7 +22,10 @@ describe("Core — Respec Global - document.respec", () => { }); it("has an array of errors and warnings", async () => { - const config = { lint: { "broken-refs-exist": true } }; + const config = { + lint: { "broken-refs-exist": true }, + specStatus: "unofficial", + }; const body = `

FAIL

diff --git a/tests/spec/w3c/defaults-spec.js b/tests/spec/w3c/defaults-spec.js index 574c30053f..634a0cb52d 100644 --- a/tests/spec/w3c/defaults-spec.js +++ b/tests/spec/w3c/defaults-spec.js @@ -12,8 +12,8 @@ describe("W3C — Defaults", () => { const doc = await makeRSDoc(ops); const rsConf = doc.defaultView.respecConfig; expect(rsConf.lint).toEqual({ + "privsec-section": false, "no-headingless-sections": true, - "privsec-section": true, "no-http-props": true, "no-unused-vars": false, "local-refs-exist": true, @@ -22,6 +22,7 @@ describe("W3C — Defaults", () => { "check-charset": false, "wpt-tests-exist": false, "no-unused-dfns": "warn", + "required-sections": true, a11y: false, }); expect(rsConf.highlightVars).toBe(true); @@ -43,6 +44,7 @@ describe("W3C — Defaults", () => { "fake-linter-rule": "foo", "check-internal-slots": true, "no-unused-dfns": "error", + "required-sections": "warn", }, license: "c0", specStatus: "ED", @@ -65,6 +67,7 @@ describe("W3C — Defaults", () => { "check-charset": false, "wpt-tests-exist": false, "no-unused-dfns": "error", + "required-sections": "warn", a11y: false, }); expect(rsConf.highlightVars).toBe(false); diff --git a/tests/spec/w3c/linter-rules/required-sections-spec.js b/tests/spec/w3c/linter-rules/required-sections-spec.js new file mode 100644 index 0000000000..5a67eca96e --- /dev/null +++ b/tests/spec/w3c/linter-rules/required-sections-spec.js @@ -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 = ` +
+

Privacy considerations

+

This is a privacy 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 = ` +
+

Security considerations

+

This is a security 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 = ` +
+

Privacy Considerations

+

This is a privacy section

+
+
+

Security Considerations

+

This is a security 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); + }); +});