@ksmv/ui-checks 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +5 -0
- package/dist/anchor.d.ts +8 -0
- package/dist/anchor.js +62 -0
- package/dist/baseline.d.ts +8 -0
- package/dist/baseline.js +44 -0
- package/dist/color.js +9 -1
- package/dist/evidence.js +30 -4
- package/dist/result.d.ts +8 -0
- package/dist/rules/noLiteralColor.js +9 -0
- package/dist/rules/tokenComplete.js +9 -1
- package/dist/rules/tokenContrast.js +1 -0
- package/dist/rules/tokenPrefix.js +3 -0
- package/dist/scan.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.5 - 2026-09-28
|
|
4
|
+
|
|
5
|
+
- **Fingerprints de baseline estáveis.** O fingerprint passa a seguir o conteúdo do achado e a ordem entre achados idênticos no mesmo arquivo, e não mais a linha e a coluna. Na 0.1.4, uma linha inserida acima de dívida conhecida fazia toda a dívida seguinte reaparecer como achado novo; numa aplicação consumidora real, um comentário no topo do CSS gerou 115 falsos achados novos. O conteúdo entra no fingerprint só como hash, e nenhum trecho é gravado ou relatado.
|
|
6
|
+
- **Baselines da 0.1.4 ou anteriores precisam ser regeneradas uma vez**, porque os fingerprints mudaram.
|
|
7
|
+
- Espaços dentro de valores entre aspas, como em `[data-state="a b"]`, continuam distinguindo achados. Uma regra de terceiros que não forneça âncora de conteúdo mantém o fingerprint posicional, para que uma mensagem genérica não faça um achado ocupar o lugar de outro.
|
|
8
|
+
- `css/no-literal-color` deixa de abortar o arquivo quando o parser de cores lança exceção em valores que ele não reconhece, como `calc(100vw-1.5rem)` num valor arbitrário do Tailwind. O valor passa a ser tratado como "não é cor".
|
|
9
|
+
|
|
10
|
+
## 0.1.4 - 2026-09-28
|
|
11
|
+
|
|
12
|
+
- A evidência calculada aceita tokens derivados ausentes ou vazios, que é como o navegador informa um token que o tema deixou no padrão, e usa o valor do token de origem. Na 0.1.3, um tema que confiava nos padrões documentados dos derivados nunca produzia evidência válida pela receita do README.
|
|
13
|
+
- Tokens de marca (`brandComposition`) também podem vir vazios na evidência, e a receita do README passa a capturá-los.
|
|
14
|
+
- Com evidência que traz o valor final de um token, `tokens/complete` deixa de marcar como não resolvidas as referências dele a tokens privados do consumidor. Na 0.1.3, uma ponte `--ui-*` que apontava para tokens do produto ficava com cobertura obrigatória pendente mesmo com evidência completa, e não podia entrar em baseline.
|
|
15
|
+
- Um token obrigatório vazio continua sendo recusado.
|
|
16
|
+
|
|
3
17
|
## 0.1.3 - 2026-09-28
|
|
4
18
|
|
|
5
19
|
- Sem mudança nas verificações: regras, contratos, códigos de saída e README são os mesmos da 0.1.2.
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ npx ksmv-ui-checks baseline ksmv-ui.config.js --write --first-seen-version=0.1.2
|
|
|
55
55
|
|
|
56
56
|
O arquivo guarda fingerprints, regra, papel responsável e meta de redução, sem copiar o trecho encontrado. Uma atualização que aumente a dívida exige `--allow-growth` e uma justificativa em arquivo via `--reason-file`. Erro operacional e cobertura obrigatória ausente nunca entram na baseline.
|
|
57
57
|
|
|
58
|
+
O fingerprint segue o conteúdo do achado, como seletor, propriedade e valor, e a ordem entre achados idênticos no mesmo arquivo, não a linha. Linhas inseridas ou removidas em outro ponto do arquivo não transformam dívida conhecida em achado novo; mudar o próprio trecho, sim. Baselines gravadas pela `0.1.4` ou anterior usavam a posição e precisam ser regeneradas uma vez com a `0.1.5`.
|
|
59
|
+
|
|
58
60
|
## Códigos de saída
|
|
59
61
|
|
|
60
62
|
- `0`: análise completa, sem violações novas;
|
|
@@ -84,6 +86,8 @@ A evidência não é uma exceção manual. O arquivo é recusado, com código `2
|
|
|
84
86
|
- a identidade de um dos contratos diverge da instalada, ou seja, o pacote React mudou depois da captura;
|
|
85
87
|
- falta algum token exigido, ou algum valor ainda contém `var(...)`.
|
|
86
88
|
|
|
89
|
+
Tokens opcionais, os derivados e os de marca, podem ficar vazios na captura: é assim que o navegador informa um token que o tema não declarou. Um derivado vazio assume o valor do token de origem, que é o padrão documentado no contrato.
|
|
90
|
+
|
|
87
91
|
A captura usa a API pública deste pacote e um navegador à sua escolha. Com Playwright, e a aplicação servida localmente:
|
|
88
92
|
|
|
89
93
|
```js
|
|
@@ -109,6 +113,7 @@ const tokens = [...new Set([
|
|
|
109
113
|
...contracts.theme.required,
|
|
110
114
|
...Object.keys(contracts.theme.derived),
|
|
111
115
|
...Object.values(contracts.theme.derived),
|
|
116
|
+
...contracts.theme.brandComposition,
|
|
112
117
|
...contracts.contrast.pairs.flatMap(({ background, foreground }) => [background, foreground]),
|
|
113
118
|
])];
|
|
114
119
|
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Declaration } from "postcss";
|
|
2
|
+
import type { Node, SourceFile } from "typescript/unstable/ast";
|
|
3
|
+
/** The selector chain and the declaration, value included. */
|
|
4
|
+
export declare function cssDeclarationAnchor(declaration: Declaration): string;
|
|
5
|
+
/** The selector chain and the property name, for findings about the name. */
|
|
6
|
+
export declare function cssPropertyAnchor(declaration: Declaration): string;
|
|
7
|
+
/** The source text of a TSX node. */
|
|
8
|
+
export declare function tsxNodeAnchor(node: Node, sourceFile: SourceFile): string;
|
package/dist/anchor.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Anchors identify a finding by its content rather than its position, so a
|
|
2
|
+
// baseline survives lines inserted or removed elsewhere in the file. They are
|
|
3
|
+
// hashed into the fingerprint and never stored or reported as text.
|
|
4
|
+
// Collapses whitespace outside quoted strings only: inside quotes, as in
|
|
5
|
+
// [data-state="a b"], whitespace is part of the value and tells findings apart.
|
|
6
|
+
function normalize(text) {
|
|
7
|
+
let result = "";
|
|
8
|
+
let quote;
|
|
9
|
+
let pendingSpace = false;
|
|
10
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
11
|
+
const character = text[index];
|
|
12
|
+
if (quote !== undefined) {
|
|
13
|
+
result += character;
|
|
14
|
+
if (character === "\\" && index + 1 < text.length) {
|
|
15
|
+
index += 1;
|
|
16
|
+
result += text[index];
|
|
17
|
+
}
|
|
18
|
+
else if (character === quote) {
|
|
19
|
+
quote = undefined;
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (/\s/.test(character)) {
|
|
24
|
+
pendingSpace = result.length > 0;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (pendingSpace) {
|
|
28
|
+
result += " ";
|
|
29
|
+
pendingSpace = false;
|
|
30
|
+
}
|
|
31
|
+
result += character;
|
|
32
|
+
if (character === '"' || character === "'" || character === "`") {
|
|
33
|
+
quote = character;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function cssContext(declaration) {
|
|
39
|
+
const context = [];
|
|
40
|
+
for (let parent = declaration.parent; parent && parent.type !== "root"; parent = parent.parent) {
|
|
41
|
+
if (parent.type === "rule") {
|
|
42
|
+
context.unshift(parent.selector);
|
|
43
|
+
}
|
|
44
|
+
else if (parent.type === "atrule") {
|
|
45
|
+
const atRule = parent;
|
|
46
|
+
context.unshift(`@${atRule.name} ${atRule.params}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
}
|
|
51
|
+
/** The selector chain and the declaration, value included. */
|
|
52
|
+
export function cssDeclarationAnchor(declaration) {
|
|
53
|
+
return normalize([...cssContext(declaration), `${declaration.prop}: ${declaration.value}`].join(" > "));
|
|
54
|
+
}
|
|
55
|
+
/** The selector chain and the property name, for findings about the name. */
|
|
56
|
+
export function cssPropertyAnchor(declaration) {
|
|
57
|
+
return normalize([...cssContext(declaration), declaration.prop].join(" > "));
|
|
58
|
+
}
|
|
59
|
+
/** The source text of a TSX node. */
|
|
60
|
+
export function tsxNodeAnchor(node, sourceFile) {
|
|
61
|
+
return normalize(node.getText(sourceFile));
|
|
62
|
+
}
|
package/dist/baseline.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export interface GrowthApproval {
|
|
|
36
36
|
errors: string[];
|
|
37
37
|
}
|
|
38
38
|
export declare function normalizeRelativePath(path: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Gives every violation a fingerprint that follows its content and its order
|
|
41
|
+
* among identical findings in the same file, not its line and column, so lines
|
|
42
|
+
* inserted or removed elsewhere do not turn baselined debt into new findings.
|
|
43
|
+
* The content comes from the rule's anchor, or the message when a rule sets
|
|
44
|
+
* none; either way only its hash is kept.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assignFingerprints(violations: Violation[]): void;
|
|
39
47
|
export declare function fingerprintViolation(violation: Violation): string;
|
|
40
48
|
export declare function baselineFor(violations: Violation[], metadata: BaselineMetadata, previous?: BaselineFile): BaselineFile;
|
|
41
49
|
export declare function validateBaseline(value: unknown): BaselineValidation;
|
package/dist/baseline.js
CHANGED
|
@@ -48,7 +48,51 @@ function compareEntries(left, right) {
|
|
|
48
48
|
left.rule.localeCompare(right.rule) ||
|
|
49
49
|
left.fingerprint.localeCompare(right.fingerprint));
|
|
50
50
|
}
|
|
51
|
+
const fingerprintPattern = /^[a-f0-9]{64}$/;
|
|
52
|
+
/**
|
|
53
|
+
* Gives every violation a fingerprint that follows its content and its order
|
|
54
|
+
* among identical findings in the same file, not its line and column, so lines
|
|
55
|
+
* inserted or removed elsewhere do not turn baselined debt into new findings.
|
|
56
|
+
* The content comes from the rule's anchor, or the message when a rule sets
|
|
57
|
+
* none; either way only its hash is kept.
|
|
58
|
+
*/
|
|
59
|
+
export function assignFingerprints(violations) {
|
|
60
|
+
const ordered = [...violations].sort((left, right) => normalizeRelativePath(left.path).localeCompare(normalizeRelativePath(right.path)) ||
|
|
61
|
+
left.line - right.line ||
|
|
62
|
+
left.column - right.column);
|
|
63
|
+
const occurrences = new Map();
|
|
64
|
+
for (const violation of ordered) {
|
|
65
|
+
// Without an anchor there is no content to follow, and a generic message
|
|
66
|
+
// could let one finding stand in for another; such a violation keeps its
|
|
67
|
+
// positional fingerprint.
|
|
68
|
+
if (violation.anchor === undefined) {
|
|
69
|
+
violation.fingerprint = positionalFingerprint(violation);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const identity = [
|
|
73
|
+
violation.rule.trim(),
|
|
74
|
+
normalizeRelativePath(violation.path),
|
|
75
|
+
normalizeNodeKind(violation.nodeKind),
|
|
76
|
+
violation.anchor,
|
|
77
|
+
];
|
|
78
|
+
const key = JSON.stringify(identity);
|
|
79
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
80
|
+
occurrences.set(key, occurrence + 1);
|
|
81
|
+
violation.fingerprint = createHash("sha256")
|
|
82
|
+
.update(JSON.stringify([2, ...identity, occurrence]))
|
|
83
|
+
.digest("hex");
|
|
84
|
+
delete violation.anchor;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
51
87
|
export function fingerprintViolation(violation) {
|
|
88
|
+
if (violation.fingerprint !== undefined && fingerprintPattern.test(violation.fingerprint)) {
|
|
89
|
+
return violation.fingerprint;
|
|
90
|
+
}
|
|
91
|
+
return positionalFingerprint(violation);
|
|
92
|
+
}
|
|
93
|
+
// The identity of a violation without an anchor, and of one built outside
|
|
94
|
+
// runChecks: its position, which only compares with others built the same way.
|
|
95
|
+
function positionalFingerprint(violation) {
|
|
52
96
|
const input = JSON.stringify([
|
|
53
97
|
1,
|
|
54
98
|
violation.rule.trim(),
|
package/dist/color.js
CHANGED
|
@@ -38,7 +38,15 @@ export function isColorBearingProperty(property) {
|
|
|
38
38
|
cssProperty === "column-rule");
|
|
39
39
|
}
|
|
40
40
|
function parsedColor(value) {
|
|
41
|
-
|
|
41
|
+
// culori throws, instead of answering "not a color", on some values Tailwind
|
|
42
|
+
// allows in arbitrary classes, such as calc(100vw-1.5rem).
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = parse(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
42
50
|
const color = parsed ? toRgb(parsed) : undefined;
|
|
43
51
|
if (!color ||
|
|
44
52
|
!Number.isFinite(color.r) ||
|
package/dist/evidence.js
CHANGED
|
@@ -38,11 +38,14 @@ function sameIdentity(actual, expected) {
|
|
|
38
38
|
actual.contractVersion === expected.contractVersion &&
|
|
39
39
|
actual.sha256 === expected.sha256;
|
|
40
40
|
}
|
|
41
|
+
// Derived tokens are not required: a theme may leave them to their documented
|
|
42
|
+
// default, the source token, and the browser then reports them as empty.
|
|
41
43
|
function requiredEvidenceTokens(contracts) {
|
|
44
|
+
const derived = contracts.theme.derived;
|
|
42
45
|
return new Set([
|
|
43
46
|
...contracts.theme.required,
|
|
44
|
-
|
|
45
|
-
...Object.values(
|
|
47
|
+
// A source that is itself derived is optional too; its chain is followed.
|
|
48
|
+
...Object.values(derived).filter((source) => !(source in derived)),
|
|
46
49
|
...contracts.contrast.pairs.flatMap(({ background, foreground }) => [
|
|
47
50
|
background,
|
|
48
51
|
foreground,
|
|
@@ -73,13 +76,20 @@ function validateEvidenceShape(value, contracts) {
|
|
|
73
76
|
!isRecord(value.themes)) {
|
|
74
77
|
return false;
|
|
75
78
|
}
|
|
79
|
+
// A token the evidence must carry is never optional, even when the contract
|
|
80
|
+
// also lists it as derived or as a brand token.
|
|
81
|
+
const required = requiredEvidenceTokens(contracts);
|
|
82
|
+
const optional = new Set([...Object.keys(contracts.theme.derived), ...contracts.theme.brandComposition]
|
|
83
|
+
.filter((token) => !required.has(token)));
|
|
76
84
|
return Object.values(value.themes).every((theme) => isRecord(theme) &&
|
|
77
85
|
exactKeys(theme, ["selector", "tokens"]) &&
|
|
78
86
|
typeof theme.selector === "string" &&
|
|
79
87
|
isRecord(theme.tokens) &&
|
|
80
88
|
Object.entries(theme.tokens).every(([token, tokenValue]) => tokenNamePattern.test(token) &&
|
|
81
89
|
typeof tokenValue === "string" &&
|
|
82
|
-
|
|
90
|
+
// Empty means undeclared, which only an optional token may be: a derived
|
|
91
|
+
// token left to its default, or a brand token the theme does not use.
|
|
92
|
+
(tokenValue.trim().length > 0 || optional.has(token)) &&
|
|
83
93
|
!/var\s*\(/i.test(tokenValue)));
|
|
84
94
|
}
|
|
85
95
|
export async function loadComputedEvidence({ contracts, evidenceByPath, now = new Date(), root, themes, }) {
|
|
@@ -121,7 +131,23 @@ export async function loadComputedEvidence({ contracts, evidenceByPath, now = ne
|
|
|
121
131
|
throw new Error("tokens");
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
|
-
|
|
134
|
+
const tokens = Object.fromEntries(Object.entries(computedTheme.tokens).filter(([, tokenValue]) => tokenValue.trim().length > 0));
|
|
135
|
+
// A derived token left to its default takes its source's value,
|
|
136
|
+
// following sources that are derived themselves. A chain that does not
|
|
137
|
+
// end in a real value makes the evidence invalid.
|
|
138
|
+
const derivedValue = (token, seen) => {
|
|
139
|
+
const value = tokens[token];
|
|
140
|
+
if (value !== undefined)
|
|
141
|
+
return value;
|
|
142
|
+
const source = contracts.theme.derived[token];
|
|
143
|
+
if (source === undefined || seen.has(token))
|
|
144
|
+
throw new Error("tokens");
|
|
145
|
+
return derivedValue(source, new Set([...seen, token]));
|
|
146
|
+
};
|
|
147
|
+
for (const token of Object.keys(contracts.theme.derived)) {
|
|
148
|
+
tokens[token] = derivedValue(token, new Set());
|
|
149
|
+
}
|
|
150
|
+
result[theme.name] = tokens;
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
153
|
return result;
|
package/dist/result.d.ts
CHANGED
|
@@ -4,6 +4,14 @@ export interface SourceLocation {
|
|
|
4
4
|
line: number;
|
|
5
5
|
}
|
|
6
6
|
export interface Violation extends SourceLocation {
|
|
7
|
+
/**
|
|
8
|
+
* Content that identifies the finding independently of its position. Rules
|
|
9
|
+
* set it; `runChecks` hashes it into `fingerprint` and removes it, so it is
|
|
10
|
+
* never reported or stored.
|
|
11
|
+
*/
|
|
12
|
+
anchor?: string;
|
|
13
|
+
/** Stable fingerprint assigned by `runChecks`; see `fingerprintViolation`. */
|
|
14
|
+
fingerprint?: string;
|
|
7
15
|
message: string;
|
|
8
16
|
nodeKind: string;
|
|
9
17
|
path: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isComputedPropertyName, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
2
|
+
import { cssDeclarationAnchor, tsxNodeAnchor } from "../anchor.js";
|
|
2
3
|
import { containsLiteralColor, isColorBearingProperty } from "../color.js";
|
|
3
4
|
import { isAllowedThemeDeclaration } from "./theme.js";
|
|
4
5
|
function emptyResult() {
|
|
@@ -56,6 +57,7 @@ export function createNoLiteralColorRule({ themes, }) {
|
|
|
56
57
|
containsLiteralColor(declaration.value) &&
|
|
57
58
|
!isAllowedThemeDeclaration(file, declaration, themes)) {
|
|
58
59
|
result.violations.push({
|
|
60
|
+
anchor: cssDeclarationAnchor(declaration),
|
|
59
61
|
column: declaration.source?.start?.column ?? 1,
|
|
60
62
|
line: declaration.source?.start?.line ?? 1,
|
|
61
63
|
message: "Literal colors are allowed only in configured themes.",
|
|
@@ -79,7 +81,13 @@ export function createNoLiteralColorRule({ themes, }) {
|
|
|
79
81
|
if (value !== undefined &&
|
|
80
82
|
((isDirectColor && containsLiteralColor(value)) || isLiteralClass)) {
|
|
81
83
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
84
|
+
// For className, only the classes that carry the literal color
|
|
85
|
+
// identify the finding; the other classes may change freely.
|
|
86
|
+
const anchor = isLiteralClass && !isDirectColor
|
|
87
|
+
? `className: ${value.split(/\s+/).filter(classContainsLiteralColor).join(" ")}`
|
|
88
|
+
: tsxNodeAnchor(node, sourceFile);
|
|
82
89
|
result.violations.push({
|
|
90
|
+
anchor,
|
|
83
91
|
column: position.character + 1,
|
|
84
92
|
line: position.line + 1,
|
|
85
93
|
message: "Literal colors are allowed only in configured themes.",
|
|
@@ -107,6 +115,7 @@ export function createNoLiteralColorRule({ themes, }) {
|
|
|
107
115
|
containsLiteralColor(property.initializer.text)) {
|
|
108
116
|
const position = sourceFile.getLineAndCharacterOfPosition(property.initializer.getStart(sourceFile));
|
|
109
117
|
result.violations.push({
|
|
118
|
+
anchor: tsxNodeAnchor(property, sourceFile),
|
|
110
119
|
column: position.character + 1,
|
|
111
120
|
line: position.line + 1,
|
|
112
121
|
message: "Literal colors are allowed only in configured themes.",
|
|
@@ -58,6 +58,7 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
58
58
|
missingRequired = true;
|
|
59
59
|
result.violations.push({
|
|
60
60
|
...location,
|
|
61
|
+
anchor: theme.name,
|
|
61
62
|
message: `Theme ${theme.name} is missing required token ${token}.`,
|
|
62
63
|
nodeKind: `theme-token:${token}`,
|
|
63
64
|
rule: "tokens/complete",
|
|
@@ -78,6 +79,7 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
78
79
|
!theme.declarations.has(fallback)) {
|
|
79
80
|
result.violations.push({
|
|
80
81
|
...location,
|
|
82
|
+
anchor: theme.name,
|
|
81
83
|
message: `Derived token ${token} has no available documented fallback.`,
|
|
82
84
|
nodeKind: `theme-token:${token}`,
|
|
83
85
|
rule: "tokens/complete",
|
|
@@ -92,7 +94,13 @@ export function createTokenCompleteRule({ computed = {}, contract, themes, }) {
|
|
|
92
94
|
tokens.set(name, value);
|
|
93
95
|
}
|
|
94
96
|
const reportedReferences = new Set();
|
|
95
|
-
for (const declaration of theme.declarations
|
|
97
|
+
for (const [name, declaration] of theme.declarations) {
|
|
98
|
+
// The evidence carries the value the browser computed for this very
|
|
99
|
+
// token, so whatever it referenced, private tokens included, has
|
|
100
|
+
// already been resolved.
|
|
101
|
+
if (computedTheme[name] !== undefined) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
96
104
|
for (const reference of unresolvedTokenReferences(declaration.value, tokens)) {
|
|
97
105
|
if (!reportedReferences.has(reference)) {
|
|
98
106
|
reportedReferences.add(reference);
|
|
@@ -157,6 +157,7 @@ export function createTokenContrastRule({ computed = {}, manifest, themes, }) {
|
|
|
157
157
|
else if (ratio < pair.minimum) {
|
|
158
158
|
result.violations.push({
|
|
159
159
|
...location,
|
|
160
|
+
anchor: theme.name,
|
|
160
161
|
message: `Contrast pair ${pair.context} is below ${pair.minimum}:1.`,
|
|
161
162
|
nodeKind: `contrast-pair:${pair.context}:${pair.foreground}:${pair.background}`,
|
|
162
163
|
rule: "tokens/contrast",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isComputedPropertyName, isIdentifier, isJsxAttribute, isJsxExpression, isObjectLiteralExpression, isPropertyAssignment, isStringLiteralLikeNode, } from "typescript/unstable/ast";
|
|
2
|
+
import { cssPropertyAnchor, tsxNodeAnchor } from "../anchor.js";
|
|
2
3
|
export function createTokenPrefixRule() {
|
|
3
4
|
return {
|
|
4
5
|
name: "tokens/prefix",
|
|
@@ -8,6 +9,7 @@ export function createTokenPrefixRule() {
|
|
|
8
9
|
if (declaration.prop.startsWith("--") &&
|
|
9
10
|
!declaration.prop.startsWith("--ui-")) {
|
|
10
11
|
violations.push({
|
|
12
|
+
anchor: cssPropertyAnchor(declaration),
|
|
11
13
|
column: declaration.source?.start?.column ?? 1,
|
|
12
14
|
line: declaration.source?.start?.line ?? 1,
|
|
13
15
|
message: "Public custom properties must use the --ui- prefix.",
|
|
@@ -42,6 +44,7 @@ export function createTokenPrefixRule() {
|
|
|
42
44
|
if (name?.startsWith("--") && !name.startsWith("--ui-")) {
|
|
43
45
|
const position = sourceFile.getLineAndCharacterOfPosition(propertyNode.getStart(sourceFile));
|
|
44
46
|
violations.push({
|
|
47
|
+
anchor: tsxNodeAnchor(propertyNode, sourceFile),
|
|
45
48
|
column: position.character + 1,
|
|
46
49
|
line: position.line + 1,
|
|
47
50
|
message: "Public custom properties must use the --ui- prefix.",
|
package/dist/scan.js
CHANGED
|
@@ -5,6 +5,7 @@ import { computeLineStarts, createScanner, LanguageVariant, SyntaxKind, } from "
|
|
|
5
5
|
import { API } from "typescript/unstable/sync";
|
|
6
6
|
import { resolveContractSet } from "./contracts.js";
|
|
7
7
|
import { loadComputedEvidence } from "./evidence.js";
|
|
8
|
+
import { assignFingerprints } from "./baseline.js";
|
|
8
9
|
import { applyDebtControls } from "./governance.js";
|
|
9
10
|
const supportedExtensions = [".css", ".js", ".jsx", ".ts", ".tsx"];
|
|
10
11
|
function toPosixPath(path) {
|
|
@@ -384,6 +385,7 @@ export async function runChecks(config, options = {}) {
|
|
|
384
385
|
}
|
|
385
386
|
typeScriptSnapshot?.dispose();
|
|
386
387
|
typeScriptApi?.close();
|
|
388
|
+
assignFingerprints(report.violations);
|
|
387
389
|
if (options.applyGovernance ?? usesDefaultRules) {
|
|
388
390
|
await applyDebtControls(report, config);
|
|
389
391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksmv/ui-checks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Static and computed quality checks for semantic UI token contracts.",
|
|
5
5
|
"keywords": ["accessibility", "design-tokens", "quality", "static-analysis"],
|
|
6
6
|
"license": "Apache-2.0",
|