Skip to content

feat(cli): Add exfig lint command for Figma file validation#80

Merged
alexey1312 merged 6 commits into
mainfrom
feature/exfig-lint
Mar 31, 2026
Merged

feat(cli): Add exfig lint command for Figma file validation#80
alexey1312 merged 6 commits into
mainfrom
feature/exfig-lint

Conversation

@alexey1312
Copy link
Copy Markdown
Collaborator

Description

  • Add exfig lint CLI command that validates Figma file structure against PKL config before export
  • 7 lint rules: frame-page-match, naming-convention, component-not-frame, deleted-variables, duplicate-component-names, dark-mode-variables, dark-mode-suffix
  • Rule engine with LintDataCache actor to deduplicate Figma API calls across rules
  • Per-rule spinner progress: "Checking Dark mode fills... (5/7)"
  • Compact table output for text mode, clean JSON for --format json (auto-quiet)
  • --rules filter, --severity threshold, exit code 1 on errors (CI-ready)
  • MCP tool exfig_lint for AI assistant integration
  • 25 unit tests with mock Figma API responses

Additional notes

  • Cross-file variable alias references (library refs) are recognized and skipped — not flagged as broken
  • RTL variant components (RTL=On/RTL=Off) are excluded from duplicate and variable checks
  • Root component frame fills (backgrounds) are skipped in dark-mode-variables — only child vector nodes checked
  • All rules filter by configured figmaFrameName + figmaPageName from PKL config entries
  • Companion Figma Plugin at DesignPipe/figma-design-linter for designer-side checks
  • exfig-action updated with command: lint support, lint_rules/lint_severity inputs, Slack notifications

Validates Figma file structure against PKL config before export.
7 rules checking naming conventions, frame/page structure, variable
bindings, dark mode setup, and component types.

Rules:
- frame-page-match: frame/page names exist in Figma file
- naming-convention: component names match nameValidateRegexp
- component-not-frame: configured frames have published components
- deleted-variables: no deletedButReferenced variables
- alias-chain-integrity: alias chains resolve without broken refs
- dark-mode-variables: fills bound to Variables when variablesDarkMode
- dark-mode-suffix: light components have matching _dark pairs

Supports --rules filter, --format json for CI, --severity filter,
and exit code 1 on errors.
Add lint command section, troubleshooting entries for config type
reference, Paint.visible, and variablesColors location.
Update exfig.usage.kdl with lint subcommand and flags.
Add Linting section to Usage.md with rules table.
Add Linting in CI section to CICDIntegration.md.
alias-chain-integrity: recognize cross-file variable IDs (long hex
hash prefix) as external library references — not broken chains.

dark-mode-suffix: only check components within configured export
frames, not all components in the entire Figma file.
21 tests covering FramePageMatchRule, NamingConventionRule,
DeletedVariablesRule, AliasChainIntegrityRule, ComponentNotFrameRule,
DarkModeSuffixRule, and LintEngine with mock Figma API responses.

refactor(lint): improve output, progress, and rules

- Add per-rule spinner progress: "Checking X... (2/7)"
- Replace verbose text output with compact table format
- Add duplicate-component-names rule (catches silent overwrites)
- Remove alias-chain-integrity from defaults (cross-file refs normal)
- Keep dark-mode-suffix as warning (not all icons need dark pairs)
- Errors sorted first, then warnings

perf(lint): cache API responses and show rule names in spinner

- Add LintDataCache actor to deduplicate Components/Variables API calls
  (5 rules shared the same ComponentsEndpoint, now 1 request per fileId)
- Add withSpinnerMessage to TerminalUI for dynamic message updates
- Spinner now shows: "Checking Dark mode fills... (6/7)"

fix(lint): downgrade deleted-variables to warning (ExFig handles them)

fix(lint): skip root frame fills in dark-mode-variables check

Root component frames often have background #FFFFFF fills that aren't
meant to be variable-bound. Only check fills on child vector nodes,
not the component's root frame.

fix(lint): filter dark-mode-variables by page name from config

fix(lint): skip RTL variant components in dark-mode-variables check

fix(lint): filter duplicate-component-names by configured frame/page

fix(lint): skip RTL variants in duplicate-component-names check

docs: add lint rule patterns and cp gotcha to CLAUDE.md

docs: add lint to ExFig.md, GettingStarted.md, and regenerate llms

feat(mcp): add exfig_lint tool for Figma structure validation

Returns JSON diagnostics with errors/warnings. Supports rule
filtering and severity threshold. Uses LintDataCache for dedup.

test(lint): add tests for duplicate-component-names rule and RTL skip

fix(lint): suppress spinner in --format json mode for clean stdout
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive lint command to validate Figma file structures against PKL configurations. It includes a new LintEngine with seven built-in rules covering naming conventions, frame/page structure, variable bindings, and dark mode setup. The command is integrated into the CLI, MCP server, and documentation. Feedback focuses on performance optimizations, such as caching compiled regular expressions and avoiding redundant dictionary constructions during component iteration.


let pageNames = Set(components.compactMap(\.containingFrame.pageName))
let frameNames = Set(components.compactMap(\.containingFrame.name))
var framesByPage: [String: Set<String>] = [:]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dictionary framesByPage is constructed by iterating over all components. If the component list is very large, this could be a performance bottleneck. Consider if this can be optimized by filtering components earlier.

Comment on lines +36 to +38
let regex: NSRegularExpression
do {
regex = try NSRegularExpression(pattern: pattern)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Compiling the regular expression inside the loop for every entry is inefficient. Move the regex compilation outside the loop or cache the compiled regex objects.

…afety

- Register AliasChainIntegrityRule in default engine (was dead code)
- Replace silent `catch { continue }` with error diagnostics in 6 rules
- Raise engine catch severity from .info to .error, propagate CancellationError
- Add figmaPageName filtering to NamingConventionRule and DarkModeSuffixRule
- Add RTL variant skip to DarkModeSuffixRule
- Use ExpressibleByArgument for --format/--severity (reject invalid values)
- Add Comparable to LintSeverity, remove duplicated severityRank()
- Add diagnostic() factory on LintRule to reduce boilerplate
- Fix Usage.md severities and add missing duplicate-component-names rule
- Add flutter.images to NamingConventionRule
- Add sampling info diagnostic in DarkModeVariablesRule
- Route JSON output through TerminalUI instead of print()
- Validate MCP lint params before expensive PKL eval
- Add engine composition, error handling, and severity filtering tests
@alexey1312 alexey1312 merged commit f3cece8 into main Mar 31, 2026
@alexey1312 alexey1312 deleted the feature/exfig-lint branch March 31, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant