figma-local 2.0.0 → 2.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
5
  "author": "elvke",
6
6
  "license": "MIT",
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: figma-component-audit
3
+ description: |
4
+ Use this skill when the user wants to audit, review, or check the quality of Figma components. Triggers on: "audit", "audit components", "audit all components", "check component quality", "what's wrong with my components", "component issues", "find problems in components", "review my design system components", "component health", "component score", "check for hardcoded colors", "missing descriptions", "detached instances", "incomplete variants". Requires a Figma file to be open and the daemon connected.
5
+ allowed-tools:
6
+ - Bash(fig component-audit *)
7
+ - Bash(fig component-audit)
8
+ - Bash(fig daemon status)
9
+ ---
10
+
11
+ # Figma Component Audit
12
+
13
+ Audit Figma components for design-system quality issues. Returns a score (0–100) and a categorized list of issues per component.
14
+
15
+ ## Prerequisites
16
+
17
+ The `fig` CLI must be connected. Check with `fig daemon status`. If not connected: `fig connect --safe`.
18
+
19
+ ## Usage
20
+
21
+ ### Audit current selection
22
+
23
+ Select a component or frame in Figma, then:
24
+
25
+ ```bash
26
+ fig component-audit
27
+ ```
28
+
29
+ ### Audit a specific component by name
30
+
31
+ ```bash
32
+ fig component-audit "Button"
33
+ fig component-audit "Card"
34
+ fig component-audit "Navigation Bar"
35
+ ```
36
+
37
+ ### Audit ALL components on the current page
38
+
39
+ ```bash
40
+ fig component-audit --all
41
+ ```
42
+
43
+ This is the most useful command for a full design-system review. It scans every `COMPONENT` and `COMPONENT_SET` on the page, ranks them by score (worst first), and prints a summary.
44
+
45
+ ### Audit by node ID
46
+
47
+ ```bash
48
+ fig component-audit --node "123:456"
49
+ ```
50
+
51
+ ### JSON output (for piping or saving)
52
+
53
+ ```bash
54
+ fig component-audit --all --json
55
+ fig component-audit --all --json > audit-report.json
56
+ ```
57
+
58
+ ### Include info-level issues
59
+
60
+ By default, only errors and warnings are shown. Add `--verbose` to also see info-level suggestions:
61
+
62
+ ```bash
63
+ fig component-audit --all --verbose
64
+ fig component-audit "Button" --verbose
65
+ ```
66
+
67
+ ## What Gets Checked
68
+
69
+ | Rule | Severity | What it flags |
70
+ |------|----------|---------------|
71
+ | `missing-description` | warning | Component has no description set |
72
+ | `incomplete-variants` | warning | Component set is missing expected variant combinations |
73
+ | `hidden-layer` | info | A child layer is hidden (dead weight in the file) |
74
+ | `generic-layer-name` | info | Layer has a default name like "Frame 2" or "Rectangle" |
75
+ | `empty-text` | warning | A text node exists but has no content |
76
+ | `hardcoded-color` | warning | A solid fill color with no variable binding |
77
+ | `no-auto-layout` | info | A frame with 2+ children but no auto layout enabled |
78
+ | `detached-instance` | error | An instance whose main component is missing |
79
+ | `deep-nesting` | info | A node nested 7+ levels deep |
80
+
81
+ ## Scoring
82
+
83
+ `score = 100 − (errors × 15) − (warnings × 5) − (info × 2)`
84
+
85
+ - **≥ 80** — Good
86
+ - **60–79** — Fair
87
+ - **< 60** — Needs work
88
+
89
+ ## Workflow: Full Design-System Audit
90
+
91
+ 1. Open the Figma file containing your component library
92
+ 2. Make sure `fig` is connected: `fig daemon status`
93
+ 3. Run the full audit:
94
+ ```bash
95
+ fig component-audit --all
96
+ ```
97
+ 4. Review the output — components sorted worst-first
98
+ 5. For a detailed look at the worst component:
99
+ ```bash
100
+ fig component-audit "ComponentName" --verbose
101
+ ```
102
+ 6. Fix the issues in Figma, then re-run to verify improvement
103
+
104
+ ## Tips
105
+
106
+ - Run `--all --json` to save a baseline report and compare over time
107
+ - `detached-instance` errors (score −15 each) are the highest priority to fix
108
+ - `hardcoded-color` warnings usually mean a token should be created in your variable collection
109
+ - Use `fig var list` to see available variable collections before fixing hardcoded colors
110
+ - `incomplete-variants` means your component set has a property with N options but fewer than the expected NxM combinations
@@ -0,0 +1,312 @@
1
+ /**
2
+ * component-audit.js — Figma component audit logic
3
+ *
4
+ * Generates Figma plugin JS code that inspects components for:
5
+ * - Naming issues (unnamed layers, generic names)
6
+ * - Missing descriptions
7
+ * - Hardcoded colors (no variable bindings)
8
+ * - Missing auto layout
9
+ * - Hidden layers (dead weight)
10
+ * - Empty text nodes
11
+ * - Excessive nesting depth (>6 levels)
12
+ * - Variant completeness for component sets
13
+ * - Detached instances within a component
14
+ */
15
+
16
+ /**
17
+ * Build the Figma JS code to audit a single component node by ID.
18
+ * @param {string} nodeId
19
+ */
20
+ export function buildSingleAuditCode(nodeId) {
21
+ return `
22
+ (function() {
23
+ var node = figma.getNodeById(${JSON.stringify(nodeId)});
24
+ if (!node) return { error: 'Node not found: ${nodeId}' };
25
+ if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
26
+ return { error: 'Node is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
27
+ }
28
+ return auditComponent(node);
29
+ ${AUDIT_HELPERS}
30
+ })()
31
+ `;
32
+ }
33
+
34
+ /**
35
+ * Build the Figma JS code to audit ALL components on the current page.
36
+ */
37
+ export function buildAllAuditCode() {
38
+ return `
39
+ (function() {
40
+ var page = figma.currentPage;
41
+ var results = [];
42
+
43
+ function collectComponents(node) {
44
+ if (node.type === 'COMPONENT_SET') {
45
+ results.push(auditComponent(node));
46
+ return; // children are COMPONENT variants — covered by set audit
47
+ }
48
+ if (node.type === 'COMPONENT') {
49
+ // Skip components that are children of a COMPONENT_SET (audited via the set)
50
+ if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
51
+ results.push(auditComponent(node));
52
+ }
53
+ return;
54
+ }
55
+ if (node.children) {
56
+ for (var i = 0; i < node.children.length; i++) {
57
+ collectComponents(node.children[i]);
58
+ }
59
+ }
60
+ }
61
+
62
+ collectComponents(page);
63
+ return {
64
+ page: page.name,
65
+ total: results.length,
66
+ components: results
67
+ };
68
+ ${AUDIT_HELPERS}
69
+ })()
70
+ `;
71
+ }
72
+
73
+ /**
74
+ * Build the Figma JS code to audit the current selection.
75
+ */
76
+ export function buildSelectionAuditCode() {
77
+ return `
78
+ (function() {
79
+ var sel = figma.currentPage.selection;
80
+ if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a component or frame in Figma first.' };
81
+ var node = sel[0];
82
+ if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
83
+ return { error: 'Selection is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
84
+ }
85
+ return auditComponent(node);
86
+ ${AUDIT_HELPERS}
87
+ })()
88
+ `;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Shared helper code injected into every eval string.
93
+ // Written as a plain string so it can be appended inside the IIFE.
94
+ // ---------------------------------------------------------------------------
95
+ const AUDIT_HELPERS = `
96
+ function auditComponent(node) {
97
+ var issues = [];
98
+ var stats = { textNodes: 0, hiddenNodes: 0, instances: 0, detachedInstances: 0, maxDepth: 0 };
99
+
100
+ // ── 1. Description check ─────────────────────────────────────────────────
101
+ if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
102
+ if (!node.description || node.description.trim() === '') {
103
+ issues.push({ rule: 'missing-description', severity: 'warning', message: 'Component has no description' });
104
+ }
105
+ }
106
+
107
+ // ── 2. Variant completeness (COMPONENT_SET only) ─────────────────────────
108
+ if (node.type === 'COMPONENT_SET') {
109
+ var propDefs = node.componentPropertyDefinitions || {};
110
+ var propKeys = Object.keys(propDefs);
111
+ var variantProps = propKeys.filter(function(k) { return propDefs[k].type === 'VARIANT'; });
112
+ if (variantProps.length > 0) {
113
+ var expected = 1;
114
+ variantProps.forEach(function(k) {
115
+ expected *= (propDefs[k].variantOptions || []).length;
116
+ });
117
+ var actual = (node.children || []).length;
118
+ if (actual < expected) {
119
+ issues.push({
120
+ rule: 'incomplete-variants',
121
+ severity: 'warning',
122
+ message: 'Variant set has ' + actual + ' of ' + expected + ' expected combinations',
123
+ details: { expected: expected, actual: actual, properties: variantProps }
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ // ── 3. Deep tree walk ────────────────────────────────────────────────────
130
+ var GENERIC_NAMES = /^(Frame|Rectangle|Ellipse|Group|Vector|Polygon|Star|Line|Image|Component)\\s*\\d*$/i;
131
+
132
+ function walk(n, depth) {
133
+ if (depth > stats.maxDepth) stats.maxDepth = depth;
134
+
135
+ // Hidden layers
136
+ if (depth > 0 && n.visible === false) {
137
+ stats.hiddenNodes++;
138
+ issues.push({ rule: 'hidden-layer', severity: 'info', message: 'Hidden layer: "' + n.name + '"', nodeId: n.id });
139
+ }
140
+
141
+ // Generic / unnamed layer
142
+ if (depth > 0 && GENERIC_NAMES.test(n.name)) {
143
+ issues.push({ rule: 'generic-layer-name', severity: 'info', message: 'Generic layer name: "' + n.name + '"', nodeId: n.id });
144
+ }
145
+
146
+ // Text nodes
147
+ if (n.type === 'TEXT') {
148
+ stats.textNodes++;
149
+ if (!n.characters || n.characters.trim() === '') {
150
+ issues.push({ rule: 'empty-text', severity: 'warning', message: 'Empty text node: "' + n.name + '"', nodeId: n.id });
151
+ }
152
+ }
153
+
154
+ // Hardcoded colors — fills with no variable binding
155
+ if (n.fills && Array.isArray(n.fills)) {
156
+ for (var i = 0; i < n.fills.length; i++) {
157
+ var fill = n.fills[i];
158
+ if (fill.type === 'SOLID' && fill.visible !== false) {
159
+ var hasBinding = n.boundVariables && n.boundVariables.fills;
160
+ if (!hasBinding) {
161
+ var r = Math.round((fill.color.r || 0) * 255);
162
+ var g = Math.round((fill.color.g || 0) * 255);
163
+ var b = Math.round((fill.color.b || 0) * 255);
164
+ var hex = '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
165
+ // Only flag non-transparent, non-white fills that look like intentional colors
166
+ if (hex !== '#ffffff' && hex !== '#000000' && !(r === g && g === b)) {
167
+ issues.push({ rule: 'hardcoded-color', severity: 'warning', message: 'Hardcoded fill color ' + hex + ' on "' + n.name + '" — consider using a variable', nodeId: n.id });
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // Missing auto layout on FRAME nodes that contain multiple children
175
+ if (n.type === 'FRAME' && depth > 0) {
176
+ var childCount = (n.children || []).length;
177
+ if (childCount >= 2 && n.layoutMode === 'NONE') {
178
+ issues.push({ rule: 'no-auto-layout', severity: 'info', message: 'Frame "' + n.name + '" has ' + childCount + ' children but no auto layout', nodeId: n.id });
179
+ }
180
+ }
181
+
182
+ // Instances (check for detached)
183
+ if (n.type === 'INSTANCE') {
184
+ stats.instances++;
185
+ if (!n.mainComponent) {
186
+ stats.detachedInstances++;
187
+ issues.push({ rule: 'detached-instance', severity: 'error', message: 'Detached instance: "' + n.name + '" — main component missing', nodeId: n.id });
188
+ }
189
+ }
190
+
191
+ // Excessive nesting
192
+ if (depth === 7) {
193
+ issues.push({ rule: 'deep-nesting', severity: 'info', message: 'Node "' + n.name + '" is nested 7+ levels deep', nodeId: n.id });
194
+ }
195
+
196
+ if (n.children) {
197
+ for (var ci = 0; ci < n.children.length; ci++) {
198
+ walk(n.children[ci], depth + 1);
199
+ }
200
+ }
201
+ }
202
+
203
+ walk(node, 0);
204
+
205
+ // ── 4. Score ─────────────────────────────────────────────────────────────
206
+ var errors = issues.filter(function(i) { return i.severity === 'error'; }).length;
207
+ var warnings = issues.filter(function(i) { return i.severity === 'warning'; }).length;
208
+ var infos = issues.filter(function(i) { return i.severity === 'info'; }).length;
209
+ var score = Math.max(0, 100 - errors * 15 - warnings * 5 - infos * 2);
210
+
211
+ return {
212
+ id: node.id,
213
+ name: node.name,
214
+ type: node.type,
215
+ score: score,
216
+ summary: { errors: errors, warnings: warnings, info: infos },
217
+ stats: stats,
218
+ issues: issues
219
+ };
220
+ }
221
+ `;
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Formatter — turns raw audit result into human-readable CLI output
225
+ // ---------------------------------------------------------------------------
226
+
227
+ /**
228
+ * Format a single component audit result for terminal output.
229
+ * @param {object} result - from auditComponent()
230
+ * @param {object} chalk - chalk instance
231
+ * @param {boolean} verbose - show info-level issues
232
+ */
233
+ export function formatAuditResult(result, chalk, verbose = true) {
234
+ if (result.error) return chalk.red('✗ ' + result.error);
235
+
236
+ const lines = [];
237
+ const scoreColor = result.score >= 80 ? chalk.green : result.score >= 60 ? chalk.yellow : chalk.red;
238
+ const scoreLabel = result.score >= 80 ? 'Good' : result.score >= 60 ? 'Fair' : 'Needs work';
239
+
240
+ lines.push(`\n${chalk.bold(result.name)} ${chalk.gray('(' + result.type + ')')}`);
241
+ lines.push(
242
+ ` Score: ${scoreColor(result.score + '/100')} ${chalk.gray('(' + scoreLabel + ')')} ` +
243
+ chalk.red(result.summary.errors + ' errors') + ' ' +
244
+ chalk.yellow(result.summary.warnings + ' warnings') + ' ' +
245
+ chalk.gray(result.summary.info + ' info')
246
+ );
247
+ lines.push(
248
+ chalk.gray(
249
+ ` Stats: ${result.stats.textNodes} text nodes, ${result.stats.instances} instances` +
250
+ (result.stats.detachedInstances ? chalk.red(` (${result.stats.detachedInstances} detached)`) : '') +
251
+ `, ${result.stats.hiddenNodes} hidden, max depth ${result.stats.maxDepth}`
252
+ )
253
+ );
254
+
255
+ const shown = result.issues.filter(i => verbose || i.severity !== 'info');
256
+ if (shown.length === 0) {
257
+ lines.push(chalk.green(' ✓ No issues found'));
258
+ } else {
259
+ lines.push('');
260
+ for (const issue of shown) {
261
+ const icon = issue.severity === 'error' ? chalk.red('✗') :
262
+ issue.severity === 'warning' ? chalk.yellow('⚠') : chalk.gray('ℹ');
263
+ const ruleTag = chalk.gray(`[${issue.rule}]`);
264
+ lines.push(` ${icon} ${issue.message} ${ruleTag}`);
265
+ }
266
+ }
267
+
268
+ return lines.join('\n');
269
+ }
270
+
271
+ /**
272
+ * Format the all-components audit result for terminal output.
273
+ * @param {object} result - { page, total, components[] }
274
+ * @param {object} chalk
275
+ * @param {boolean} verbose
276
+ */
277
+ export function formatAllAuditResult(result, chalk, verbose = false) {
278
+ if (result.error) return chalk.red('✗ ' + result.error);
279
+
280
+ const lines = [];
281
+ const comps = result.components || [];
282
+ const totalErrors = comps.reduce((s, c) => s + c.summary.errors, 0);
283
+ const totalWarnings = comps.reduce((s, c) => s + c.summary.warnings, 0);
284
+ const avgScore = comps.length ? Math.round(comps.reduce((s, c) => s + c.score, 0) / comps.length) : 0;
285
+
286
+ lines.push('');
287
+ lines.push(chalk.bold(`Component Audit — ${result.page}`));
288
+ lines.push(
289
+ ` ${comps.length} component${comps.length !== 1 ? 's' : ''} scanned ` +
290
+ chalk.red(totalErrors + ' errors') + ' ' +
291
+ chalk.yellow(totalWarnings + ' warnings') + ' ' +
292
+ `Avg score: ${avgScore}/100`
293
+ );
294
+ lines.push('');
295
+
296
+ // Sort: worst score first
297
+ const sorted = [...comps].sort((a, b) => a.score - b.score);
298
+
299
+ for (const comp of sorted) {
300
+ lines.push(formatAuditResult(comp, chalk, verbose));
301
+ }
302
+
303
+ lines.push('');
304
+ lines.push(chalk.gray('─'.repeat(60)));
305
+ lines.push(
306
+ `${comps.filter(c => c.score >= 80).length} good ` +
307
+ `${comps.filter(c => c.score >= 60 && c.score < 80).length} fair ` +
308
+ `${comps.filter(c => c.score < 60).length} need work`
309
+ );
310
+
311
+ return lines.join('\n');
312
+ }
package/src/index.js CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  STAGE1_METADATA, buildFrameStructureCode, buildUsedTokensCode, formatLeanContext
21
21
  } from './read.js';
22
22
  import { generatePrompt } from './prompt-templates.js';
23
+ import {
24
+ buildSingleAuditCode, buildAllAuditCode, buildSelectionAuditCode,
25
+ formatAuditResult, formatAllAuditResult
26
+ } from './component-audit.js';
23
27
  import {
24
28
  nullDevice, killPort, getPortPid, sleepAfterStop,
25
29
  startFigmaApp, killFigmaApp,
@@ -11199,4 +11203,121 @@ Examples:
11199
11203
  }
11200
11204
  });
11201
11205
 
11206
+ // ─── Component Audit ────────────────────────────────────────────────────────
11207
+
11208
+ program
11209
+ .command('component-audit [name]')
11210
+ .description('Audit components for quality issues (naming, tokens, layout, variants, detached instances)')
11211
+ .option('--all', 'Audit ALL components on the current page')
11212
+ .option('--node <nodeId>', 'Audit a specific component by node ID')
11213
+ .option('--json', 'Output raw JSON')
11214
+ .option('--verbose', 'Show info-level issues in addition to errors/warnings')
11215
+ .addHelpText('after', `
11216
+ Checks run on every component:
11217
+ missing-description No description set on the component
11218
+ incomplete-variants Component set is missing expected variant combinations
11219
+ hidden-layer Child layer is hidden (dead weight)
11220
+ generic-layer-name Layer has a default name like "Frame 2" or "Rectangle"
11221
+ empty-text Text node with no content
11222
+ hardcoded-color Solid fill with no variable binding
11223
+ no-auto-layout Frame with 2+ children but no auto layout
11224
+ detached-instance Instance whose main component is missing
11225
+ deep-nesting Node nested 7+ levels deep
11226
+
11227
+ Severity:
11228
+ error Must fix — structural or correctness problem
11229
+ warning Should fix — design system or quality concern
11230
+ info Nice to fix — best practice recommendation (shown with --verbose)
11231
+
11232
+ Examples:
11233
+ fig component-audit Audit current selection
11234
+ fig component-audit "Button" Audit the component named "Button"
11235
+ fig component-audit --all Audit every component on this page
11236
+ fig component-audit --node "123:456" Audit by node ID
11237
+ fig component-audit --all --json Machine-readable output
11238
+ fig component-audit --all --verbose Include info-level issues
11239
+ `)
11240
+ .action(async (name, options) => {
11241
+ await checkConnection();
11242
+ const spinner = ora('Running component audit...').start();
11243
+
11244
+ try {
11245
+ // ── Determine which audit code to run ──────────────────────────────────
11246
+ let code;
11247
+ let isAll = false;
11248
+
11249
+ if (options.all) {
11250
+ isAll = true;
11251
+ code = buildAllAuditCode();
11252
+ spinner.text = 'Auditing all components on page...';
11253
+ } else if (options.node) {
11254
+ code = buildSingleAuditCode(options.node);
11255
+ spinner.text = `Auditing node ${options.node}...`;
11256
+ } else if (name) {
11257
+ // Find component by name via metadata, then audit it
11258
+ spinner.text = `Looking up component "${name}"...`;
11259
+ const findCode = `
11260
+ (function() {
11261
+ function findByName(node, n) {
11262
+ if ((node.type === 'COMPONENT' || node.type === 'COMPONENT_SET' || node.type === 'FRAME') &&
11263
+ node.name.toLowerCase() === n.toLowerCase()) return node.id;
11264
+ if (node.children) {
11265
+ for (var i = 0; i < node.children.length; i++) {
11266
+ var found = findByName(node.children[i], n);
11267
+ if (found) return found;
11268
+ }
11269
+ }
11270
+ return null;
11271
+ }
11272
+ return findByName(figma.currentPage, ${JSON.stringify(name)});
11273
+ })()
11274
+ `;
11275
+ const nodeId = await daemonExec('eval', { code: findCode });
11276
+ if (!nodeId) {
11277
+ spinner.fail(`No component named "${name}" found on the current page.`);
11278
+ process.exit(1);
11279
+ }
11280
+ code = buildSingleAuditCode(nodeId);
11281
+ spinner.text = `Auditing "${name}"...`;
11282
+ } else {
11283
+ // Default: audit current selection
11284
+ code = buildSelectionAuditCode();
11285
+ spinner.text = 'Auditing selection...';
11286
+ }
11287
+
11288
+ const result = await daemonExec('eval', { code });
11289
+
11290
+ if (result && result.error) {
11291
+ spinner.fail(result.error);
11292
+ process.exit(1);
11293
+ }
11294
+
11295
+ // ── Output ─────────────────────────────────────────────────────────────
11296
+ if (isAll) {
11297
+ const comps = result.components || [];
11298
+ spinner.succeed(
11299
+ `Audited ${comps.length} component${comps.length !== 1 ? 's' : ''} on page "${result.page}"`
11300
+ );
11301
+ if (options.json) {
11302
+ console.log(JSON.stringify(result, null, 2));
11303
+ } else {
11304
+ console.log(formatAllAuditResult(result, chalk, options.verbose || false));
11305
+ }
11306
+ } else {
11307
+ const score = result.score;
11308
+ const label = score >= 80 ? 'Good' : score >= 60 ? 'Fair' : 'Needs work';
11309
+ spinner.succeed(`Audited "${result.name}" — score ${score}/100 (${label})`);
11310
+ if (options.json) {
11311
+ console.log(JSON.stringify(result, null, 2));
11312
+ } else {
11313
+ console.log(formatAuditResult(result, chalk, options.verbose !== false));
11314
+ }
11315
+ }
11316
+ } catch (e) {
11317
+ spinner.fail(`Audit failed: ${e.message}`);
11318
+ process.exit(1);
11319
+ }
11320
+ });
11321
+
11202
11322
  program.parse();
11323
+