core/validators/css.validator.ts
core/validators/css.validator.ts is a file in GovLab Quality. 130 lines of code and 22 definitions.
import { CSS_EXTENSIONS, PAGES_SEGMENT, SCRIPT_EXTENSIONS } from "#configuration/constants/validation.constants";
import type { FileInput, ProjectValidator } from "#types/validation.types";
import { componentsInFile, definedVars, scriptVarRefs, usedVars } from "#core/parsers/css.parser";
import {
danglingVar,
danglingVarFix,
deadVar,
deadVarFix,
duplicateComponent,
duplicateComponentFix,
} from "#configuration/strings/validation.strings";
import type { Finding } from "#types/finding.types";
import { defineValidator } from "#core/registries/validation.registry";
import { projectFinding } from "#core/factories/finding.factory";
const CSS_SET: ReadonlySet<string> = new Set(CSS_EXTENSIONS);
const VAR_AWARE_EXTENSIONS = [...CSS_EXTENSIONS, ...SCRIPT_EXTENSIONS];
interface VarGraph {
defs: Map<string, Set<string>>;
uses: Map<string, Set<string>>;
scriptRefs: Set<string>;
}
const extensionOf = function extensionOf(path: string): string {
const dot = path.lastIndexOf(".");
return dot === -1 ? "" : path.slice(dot + 1).toLowerCase();
};
const add = function add(map: Map<string, Set<string>>, name: string, path: string): void {
map.set(name, (map.get(name) ?? new Set<string>()).add(path));
};
const buildGraph = function buildGraph(files: readonly FileInput[]): VarGraph {
const graph: VarGraph = { defs: new Map(), scriptRefs: new Set(), uses: new Map() };
for (const file of files) {
const isCss = CSS_SET.has(extensionOf(file.path));
for (const name of isCss ? definedVars(file.content) : []) {
add(graph.defs, name, file.path);
}
for (const name of isCss ? usedVars(file.content) : []) {
add(graph.uses, name, file.path);
}
for (const name of isCss ? [] : scriptVarRefs(file.content)) {
graph.scriptRefs.add(name);
}
}
return graph;
};
const unmatched = function unmatched(
source: Map<string, Set<string>>,
other: Map<string, Set<string>>,
scriptRefs: ReadonlySet<string>,
): [string, string][] {
return [...source]
.filter(([name]) => !other.has(name) && !scriptRefs.has(name))
.flatMap(([name, paths]) => [...paths].map((path): [string, string] => [name, path]));
};
const isPage = function isPage(path: string): boolean {
return `/${path.split("\\").join("/")}`.includes(PAGES_SEGMENT);
};
const duplicateComponents: ProjectValidator = {
appliesTo: CSS_EXTENSIONS,
id: "css-duplicate-components",
meta: {
canonical: ["duplicate-code", "css-architecture"],
description: "Flag a base component defined across multiple component files",
},
validate(files): Finding[] {
const byComponent = new Map<string, string[]>();
for (const file of files.filter((entry) => !isPage(entry.path))) {
for (const component of componentsInFile(file.content)) {
byComponent.set(component, [...(byComponent.get(component) ?? []), file.path]);
}
}
return [...byComponent]
.filter(([, paths]) => paths.length > 1)
.flatMap(([component, paths]) =>
paths.map((path) =>
projectFinding({
file: path,
message: duplicateComponent(component, paths.length),
ruleId: "css-duplicate-components",
suggestion: duplicateComponentFix(component),
}),
),
);
},
};
const deadVars: ProjectValidator = {
appliesTo: VAR_AWARE_EXTENSIONS,
id: "css-dead-var",
meta: {
canonical: ["no-unused", "design-tokens"],
description:
"Flag a custom property defined but never referenced via var() anywhere, the members that import the definitions included",
},
validate(files, consumers): Finding[] {
const own = buildGraph(files);
const reached = buildGraph(consumers);
const uses = new Map([...own.uses, ...reached.uses]);
const scriptRefs = new Set([...own.scriptRefs, ...reached.scriptRefs]);
return unmatched(own.defs, uses, scriptRefs).map(([name, path]) =>
projectFinding({
file: path,
message: deadVar(name),
ruleId: "css-dead-var",
suggestion: deadVarFix(name),
}),
);
},
};
const danglingVars: ProjectValidator = {
appliesTo: VAR_AWARE_EXTENSIONS,
id: "css-dangling-var",
meta: {
canonical: ["design-tokens"],
description: "Flag a var() reference whose custom property is never defined anywhere",
},
validate(files): Finding[] {
const { defs, scriptRefs, uses } = buildGraph(files);
return unmatched(uses, defs, scriptRefs).map(([name, path]) =>
projectFinding({
file: path,
message: danglingVar(name),
ruleId: "css-dangling-var",
suggestion: danglingVarFix(name),
}),
);
},
};
const CSS_VALIDATORS: readonly ProjectValidator[] = Object.freeze([duplicateComponents, deadVars, danglingVars]);
for (const validator of CSS_VALIDATORS) {
defineValidator(validator);
}