Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
refactor(linter/prefer-xref): simplify and fix severity support
- Import profiles from xref.js instead of duplicating the constant
- Add error severity support (conf.lint["prefer-xref"] = "error")
- Merge falsy and true xref guards into single early return
- Remove dead ?? "" fallback (selector guarantees data-cite exists)
- Remove unnecessary ?. on Map.get() after guaranteed set
- Trim verbose JSDoc to essential type annotations
  • Loading branch information
marcoscaceres committed May 10, 2026
commit 831ee209230b397802d61cab2b39de10418f43c9
71 changes: 13 additions & 58 deletions src/core/linter-rules/prefer-xref.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
// @ts-check
/**
* Linter rule "prefer-xref".
*
* Warns when an author uses `data-cite="SPEC#fragment"` to link to a term in a
* specification that is already covered by the configured xref database. In
* that case the xref shorthand syntax (`[= term =]`, `{{ IDL }}`, etc.) is
* both shorter and more robust than a hard-coded fragment identifier.
*/
import { docLink, getIntlData, showWarning } from "../utils.js";
import { docLink, getIntlData, showError, showWarning } from "../utils.js";
import { profiles } from "../xref.js";

const ruleName = "prefer-xref";
export const name = "core/linter-rules/prefer-xref";

/**
* Named xref profiles and their spec lists.
* Keep in sync with the `profiles` object in `src/core/xref.js`.
*
* @type {Record<string, string[]>}
*/
const PROFILES = {
"web-platform": ["HTML", "INFRA", "URL", "WEBIDL", "DOM", "FETCH"],
};

/** @satisfies {Record<string, { msg(specKey: string): string; readonly hint: string }>} */
const localizationStrings = {
en: {
Expand All @@ -36,31 +19,19 @@ const localizationStrings = {
const l10n = getIntlData(localizationStrings);

/**
* Derive the set of spec shortnames (uppercased) that the author has
* configured xref to search, based on the raw `conf.xref` value.
*
* Returns `null` when xref is enabled but no specific spec list is
* configured (`conf.xref === true`), meaning we cannot determine coverage
* without a network call and therefore skip the check.
*
* @param {Conf["xref"]} xref
* @returns {Set<string> | null}
*/
function getXrefSpecSet(xref) {
if (!xref) return null;
if (!xref || xref === true) return null;

/** @type {Set<string>} */
const specs = new Set();

if (xref === true) {
// No specific spec list — skip the check to avoid false positives.
return null;
}

if (typeof xref === "string") {
const profile = xref.toLowerCase();
if (profile in PROFILES) {
PROFILES[profile].forEach(s => specs.add(s.toUpperCase()));
if (profile in profiles) {
profiles[profile].forEach(s => specs.add(s.toUpperCase()));
}
return specs.size ? specs : null;
}
Expand All @@ -75,8 +46,8 @@ function getXrefSpecSet(xref) {
/** @type {{ profile?: string; specs?: string[] }} */ (xref);
if (profile) {
const key = profile.toLowerCase();
if (key in PROFILES) {
PROFILES[key].forEach(s => specs.add(s.toUpperCase()));
if (key in profiles) {
profiles[key].forEach(s => specs.add(s.toUpperCase()));
}
}
if (specList) {
Expand All @@ -89,22 +60,11 @@ function getXrefSpecSet(xref) {
}

/**
* Extract the spec shortname (the part before `#`, `/`, or any `?`/`!` prefix)
* from a raw `data-cite` value.
*
* Examples:
* "HTML#foo" → "HTML"
* "?HTML#foo" → "HTML"
* "HTML/path#foo" → "HTML"
*
* @param {string} rawCite
* @returns {string}
*/
function extractSpecKey(rawCite) {
return rawCite
.replace(/^[?!]/, "") // strip leading normative/informative marker
.split(/[/#]/)[0] // take the part before any "/" or "#"
.toUpperCase();
return rawCite.replace(/^[?!]/, "").split(/[/#]/)[0].toUpperCase();
Comment thread
marcoscaceres marked this conversation as resolved.
}

/**
Expand All @@ -117,41 +77,36 @@ export function run(conf) {
}

if (!conf.xref) {
// xref is not enabled; data-cite is the only option.
return;
}

const xrefSpecSet = getXrefSpecSet(conf.xref);
if (!xrefSpecSet) {
// Either xref===true (no spec list) or an unrecognised config shape.
// Skip to avoid false positives.
return;
}

// Select elements where the author has written data-cite="SPEC#fragment"
// (the "#" is in the attribute value itself, not in data-cite-frag).
// Exclude self-referencing fragments (#only) and already lint-ignored elements.
/** @type {NodeListOf<HTMLElement>} */
const elems = document.querySelectorAll(
":is(a, dfn)[data-cite*='#']:not([data-cite^='#']):not(.lint-ignore)"
);

/** @type {Map<string, HTMLElement[]>} key → offending elements */
/** @type {Map<string, HTMLElement[]>} */
const offenders = new Map();

elems.forEach(elem => {
const rawCite = elem.dataset.cite ?? "";
const rawCite = elem.dataset.cite;
const specKey = extractSpecKey(rawCite);
if (!specKey || !xrefSpecSet.has(specKey)) return;

if (!offenders.has(specKey)) {
offenders.set(specKey, []);
}
offenders.get(specKey)?.push(elem);
offenders.get(specKey).push(elem);
});
Comment thread
marcoscaceres marked this conversation as resolved.

const logger = conf.lint?.[ruleName] === "error" ? showError : showWarning;
offenders.forEach((elements, specKey) => {
showWarning(l10n.msg(specKey), name, {
logger(l10n.msg(specKey), name, {
hint: l10n.hint,
elements,
});
Expand Down
2 changes: 1 addition & 1 deletion src/core/xref.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { sub } from "./pubsubhub.js";

export const name = "core/xref";

const profiles = {
export const profiles = {
"web-platform": ["HTML", "INFRA", "URL", "WEBIDL", "DOM", "FETCH"],
};

Expand Down
Loading