feat(linter): add unknown-key rule for unknown top-level keys#84
feat(linter): add unknown-key rule for unknown top-level keys#84ryo-manba wants to merge 3 commits into
Conversation
|
Hey @ryo-manba! Thank you so much for the issue and PR. I agree this is useful for typos. However, we need to consider the primary case of custom token values in DESIGN.md. We don't require anything but The only one conditional requirement is in So the idea here would be to identify typos for a warn rather than any unknown token values because we shouldn't trigger warn logs for custom fields. Here's what I would explore for a typo-based warn rule. The Derive keys from the schema, don't hardcode themThis PR currently hardcodes // packages/cli/src/linter/parser/spec.ts
/** Canonical top-level YAML keys per the DESIGN.md schema. */
export const SCHEMA_KEYS = [
'version', 'name', 'description',
'colors', 'typography', 'rounded', 'spacing', 'components',
] as const;
export type SchemaKey = typeof SCHEMA_KEYS[number];This gives us runtime keys that also work for type inference. Add a zero-dependency Levenshtein functionThis is ~15 lines, so no npm dependency needed: // packages/cli/src/linter/linter/rules/levenshtein.ts
/** Levenshtein edit distance between two strings. */
export function levenshtein(a: string, b: string): number {
const m = a.length, n = b.length;
const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
);
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
dp[i][j] = a[i - 1] === b[j - 1]
? dp[i - 1][j - 1]
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
return dp[m][n];
}Rewrite the rule: only warn when a close match exists// packages/cli/src/linter/linter/rules/unknown-key.ts
import { SCHEMA_KEYS } from '../../parser/spec.js';
import { levenshtein } from './levenshtein.js';
import type { DesignSystemState } from '../../model/spec.js';
import type { RuleDescriptor, RuleFinding } from './types.js';
/** Max edit distance to consider a typo (not a custom key). */
const MAX_TYPO_DISTANCE = 2;
export function unknownKey(state: DesignSystemState): RuleFinding[] {
const knownSet = new Set<string>(SCHEMA_KEYS);
return (state.unknownKeys ?? []).flatMap(key => {
if (knownSet.has(key)) return [];
// Find closest known key
let bestMatch: string | undefined;
let bestDist = Infinity;
for (const known of SCHEMA_KEYS) {
const dist = levenshtein(key.toLowerCase(), known.toLowerCase());
if (dist < bestDist) {
bestDist = dist;
bestMatch = known;
}
}
if (bestDist <= MAX_TYPO_DISTANCE && bestMatch) {
return [{
path: key,
message: `Unknown key "${key}" — did you mean "${bestMatch}"?`,
}];
}
// Not close to any known key → intentional extension, stay silent
return [];
});
}Key heuristics
Test cases to cover |
davideast
left a comment
There was a problem hiding this comment.
Requesting changes based on the larger comment I left.
DESIGN.md is intentionally extensible, so warning on every unknown top-level key flags legitimate custom fields. Restrict the rule to likely typos of known schema keys (edit distance ≤ 2, case-insensitive) and stay silent for unrelated extension keys. Per @davideast review feedback on google-labs-code#84: - Add SCHEMA_KEYS / SchemaKey in parser/spec.ts as the single source of truth and reference it from the model handler. - Add a zero-dependency Levenshtein helper. - Suggest the closest known key in the warning message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESIGN.md is intentionally extensible, so warning on every unknown top-level key flags legitimate custom fields. Restrict the rule to likely typos of known schema keys (edit distance ≤ 2, case-insensitive) and stay silent for unrelated extension keys. Per @davideast review feedback on google-labs-code#84: - Add SCHEMA_KEYS / SchemaKey in parser/spec.ts as the single source of truth and reference it from the model handler. - Add a zero-dependency Levenshtein helper. - Suggest the closest known key in the warning message.
0f3e337 to
9ebf3a2
Compare
Cover empty strings, single edit operations (insert/delete/substitute), symmetry, the classic kitten/sitting case, and the exact distances used by the unknown-key typo threshold.
|
@davideast Thanks for the review! I've fixed it. |
Closes #83
Adds an
unknown-keylint rule (warning) that flags top-level YAML keys not in the known schema, so a typo likecolours:is reported instead of silently discarded.