-
Notifications
You must be signed in to change notification settings - Fork 1
feat: non-standard-naming rule — detect dev-unfriendly state names (#159) #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8255eac
63b58bd
526fe17
8b65d11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import type { RuleCheckFn, RuleDefinition } from "../../contracts/rule.js"; | ||
| import { defineRule } from "../rule-registry.js"; | ||
| import { defaultNameMsg, getDefaultNameSubType, nonSemanticNameMsg, inconsistentNamingMsg } from "../rule-messages.js"; | ||
| import { isExcludedName, isDefaultName, isNonSemanticName } from "../node-semantics.js"; | ||
| import { defaultNameMsg, getDefaultNameSubType, nonSemanticNameMsg, inconsistentNamingMsg, nonStandardNamingMsg } from "../rule-messages.js"; | ||
| import { isExcludedName, isDefaultName, isNonSemanticName, STANDARD_STATE_NAMES, STATE_NAME_SUGGESTIONS, STATE_LIKE_PATTERN } from "../node-semantics.js"; | ||
|
|
||
| function detectNamingConvention(name: string): string | null { | ||
| if (/^[a-z]+(-[a-z]+)*$/.test(name)) return "kebab-case"; | ||
|
|
@@ -141,3 +141,58 @@ export const inconsistentNamingConvention = defineRule({ | |
| check: inconsistentNamingConventionCheck, | ||
| }); | ||
|
|
||
| // ============================================ | ||
| // non-standard-naming | ||
| // ============================================ | ||
|
|
||
| const nonStandardNamingDef: RuleDefinition = { | ||
| id: "non-standard-naming", | ||
| name: "Non-Standard Naming", | ||
| category: "minor", | ||
| why: "Non-standard state names prevent interaction rules from detecting state variants — AI cannot generate correct :hover/:active/:disabled styles", | ||
| impact: "Interaction state detection fails, resulting in static UI with no state transitions", | ||
| fix: "Use platform-standard state names: hover, active, pressed, selected, highlighted, disabled, focus, focused", | ||
| }; | ||
|
|
||
| const nonStandardNamingCheck: RuleCheckFn = (node, context) => { | ||
| // Only check COMPONENT_SET (variant container) | ||
| if (node.type !== "COMPONENT_SET") return null; | ||
| if (!node.componentPropertyDefinitions) return null; | ||
|
|
||
| for (const prop of Object.values(node.componentPropertyDefinitions)) { | ||
| const p = prop as Record<string, unknown>; | ||
| if (p["type"] !== "VARIANT") continue; | ||
| const options = p["variantOptions"]; | ||
| if (!Array.isArray(options)) continue; | ||
|
|
||
| for (const opt of options) { | ||
| if (typeof opt !== "string") continue; | ||
| const lower = opt.toLowerCase().trim(); | ||
|
|
||
| // Skip if it's a standard name | ||
| if (STANDARD_STATE_NAMES.has(lower)) continue; | ||
|
|
||
| // Check if it matches a known non-standard state name | ||
| if (STATE_LIKE_PATTERN.test(opt)) { | ||
| const suggestion = STATE_NAME_SUGGESTIONS[lower]; | ||
| if (suggestion) { | ||
| return { | ||
| ruleId: nonStandardNamingDef.id, | ||
| subType: "state-name" as const, | ||
| nodeId: node.id, | ||
| nodePath: context.path.join(" > "), | ||
| message: nonStandardNamingMsg.stateName(node.name, opt, suggestion), | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| }; | ||
|
Comment on lines
+157
to
+192
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against undefined
Suggested fix const nonStandardNamingCheck: RuleCheckFn = (node, context) => {
// Only check COMPONENT_SET (variant container)
if (node.type !== "COMPONENT_SET") return null;
+ if (!node.name) return null;
if (!node.componentPropertyDefinitions) return null;🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping as-is.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 554
✏️ Learnings added
🧠 Learnings used |
||
|
|
||
| export const nonStandardNaming = defineRule({ | ||
| definition: nonStandardNamingDef, | ||
| check: nonStandardNamingCheck, | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { makeNode, makeFile, makeContext } from "../test-helpers.js"; | ||
| import { nonStandardNaming } from "./index.js"; | ||
|
|
||
| describe("non-standard-naming", () => { | ||
| it("has correct rule definition metadata", () => { | ||
| const def = nonStandardNaming.definition; | ||
| expect(def.id).toBe("non-standard-naming"); | ||
| expect(def.category).toBe("minor"); | ||
| }); | ||
|
|
||
| it("flags non-standard state name 'Clicked'", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Button", | ||
| type: "COMPONENT_SET", | ||
| componentPropertyDefinitions: { | ||
| "State": { type: "VARIANT", variantOptions: ["Default", "Clicked", "Disabled"] }, | ||
| }, | ||
| }); | ||
| const ctx = makeContext(); | ||
| const result = nonStandardNaming.check(node, ctx); | ||
|
|
||
| expect(result).not.toBeNull(); | ||
| expect(result!.subType).toBe("state-name"); | ||
| expect(result!.message).toContain("Clicked"); | ||
| expect(result!.message).toContain("pressed"); | ||
| }); | ||
|
|
||
| it("flags 'Inactive' → suggests 'disabled'", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Toggle", | ||
| type: "COMPONENT_SET", | ||
| componentPropertyDefinitions: { | ||
| "State": { type: "VARIANT", variantOptions: ["On", "Inactive"] }, | ||
| }, | ||
| }); | ||
| const ctx = makeContext(); | ||
| const result = nonStandardNaming.check(node, ctx); | ||
|
|
||
| expect(result).not.toBeNull(); | ||
| expect(result!.message).toContain("On"); | ||
| expect(result!.message).toContain("active"); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| it("passes when all state names are standard", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Button", | ||
| type: "COMPONENT_SET", | ||
| componentPropertyDefinitions: { | ||
| "State": { type: "VARIANT", variantOptions: ["Default", "Hover", "Pressed", "Disabled"] }, | ||
| }, | ||
| }); | ||
| const ctx = makeContext(); | ||
| expect(nonStandardNaming.check(node, ctx)).toBeNull(); | ||
| }); | ||
|
|
||
| it("passes for non-state variant properties", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Button", | ||
| type: "COMPONENT_SET", | ||
| componentPropertyDefinitions: { | ||
| "Size": { type: "VARIANT", variantOptions: ["Small", "Medium", "Large"] }, | ||
| }, | ||
| }); | ||
| const ctx = makeContext(); | ||
| expect(nonStandardNaming.check(node, ctx)).toBeNull(); | ||
| }); | ||
|
|
||
| it("skips non-COMPONENT_SET nodes", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Button", | ||
| type: "INSTANCE", | ||
| }); | ||
| const ctx = makeContext(); | ||
| expect(nonStandardNaming.check(node, ctx)).toBeNull(); | ||
| }); | ||
|
|
||
| it("accepts platform-standard names (pressed, selected, highlighted, focused)", () => { | ||
| const node = makeNode({ | ||
| id: "1:1", | ||
| name: "Nav Item", | ||
| type: "COMPONENT_SET", | ||
| componentPropertyDefinitions: { | ||
| "State": { type: "VARIANT", variantOptions: ["Default", "Pressed", "Selected", "Highlighted", "Focused"] }, | ||
| }, | ||
| }); | ||
| const ctx = makeContext(); | ||
| expect(nonStandardNaming.check(node, ctx)).toBeNull(); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.