@contractkit/prettier-plugin 0.9.0 → 0.9.2

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/src/print-ck.ts CHANGED
@@ -8,6 +8,18 @@ export const DEFAULT_PRINT_WIDTH = 80;
8
8
 
9
9
  // ─── Options block ──────────────────────────────────────────────────────────
10
10
 
11
+ /**
12
+ * Quote an options-block value if it isn't a plain identifier.
13
+ *
14
+ * Plain identifiers (starts with letter/underscore/dollar, rest are
15
+ * alphanumeric/underscore/dollar/hyphen/dot) are left bare. Everything
16
+ * else — paths with slashes, values starting with `#`, values with spaces,
17
+ * etc. — is double-quoted so the round-trip parse is unambiguous.
18
+ */
19
+ function quoteOptionsValue(value: string): string {
20
+ return /^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(value) ? value : `"${value}"`;
21
+ }
22
+
11
23
  function printOptionsBlock(ast: CkRootNode): string | null {
12
24
  const hasMeta = Object.keys(ast.meta).length > 0;
13
25
  const hasServices = Object.keys(ast.services).length > 0;
@@ -22,8 +34,7 @@ function printOptionsBlock(ast: CkRootNode): string | null {
22
34
  if (hasMeta) {
23
35
  lines.push(`${INDENT}keys: {`);
24
36
  for (const [key, value] of Object.entries(ast.meta)) {
25
- const v = value.startsWith('#') || value.includes(' ') ? `"${value}"` : value;
26
- lines.push(`${INDENT}${INDENT}${key}: ${v}`);
37
+ lines.push(`${INDENT}${INDENT}${key}: ${quoteOptionsValue(value)}`);
27
38
  }
28
39
  lines.push(`${INDENT}}`);
29
40
  }
@@ -31,8 +42,7 @@ function printOptionsBlock(ast: CkRootNode): string | null {
31
42
  if (hasServices) {
32
43
  lines.push(`${INDENT}services: {`);
33
44
  for (const [key, value] of Object.entries(ast.services)) {
34
- const v = value.startsWith('#') || value.includes(' ') ? `"${value}"` : value;
35
- lines.push(`${INDENT}${INDENT}${key}: ${v}`);
45
+ lines.push(`${INDENT}${INDENT}${key}: ${quoteOptionsValue(value)}`);
36
46
  }
37
47
  lines.push(`${INDENT}}`);
38
48
  }
@@ -69,6 +79,14 @@ function printOptionsHeaderScope(keyword: 'request' | 'response', headers: OpRes
69
79
 
70
80
  // ─── CK file printer ───────────────────────────────────────────────────────
71
81
 
82
+ /**
83
+ * Render a parsed `.ck` AST back to source. Output is byte-identical on
84
+ * round-trip when the input is already canonically formatted: options block
85
+ * first, then contracts, then operations, separated by blank lines.
86
+ *
87
+ * `printWidth` is forwarded to per-model printing for line wrapping inside
88
+ * inline-object types.
89
+ */
72
90
  export function printCk(ast: CkRootNode, printWidth: number = DEFAULT_PRINT_WIDTH): string {
73
91
  const parts: string[] = [];
74
92
 
package/src/print-type.ts CHANGED
@@ -3,6 +3,7 @@ import { INDENT } from './indent.js';
3
3
 
4
4
  // ─── Type expression printer ────────────────────────────────────────────────
5
5
 
6
+ /** Render a `ContractTypeNode` back to its `.ck` source string. */
6
7
  export function printType(type: ContractTypeNode): string {
7
8
  switch (type.kind) {
8
9
  case 'scalar': {
@@ -29,7 +30,7 @@ export function printType(type: ContractTypeNode): string {
29
30
  case 'record':
30
31
  return `record(${printType(type.key)}, ${printType(type.value)})`;
31
32
  case 'enum':
32
- return `enum(${type.values.join(', ')})`;
33
+ return `enum(${type.values.map(formatEnumValue).join(', ')})`;
33
34
  case 'literal':
34
35
  return typeof type.value === 'string' ? `literal("${type.value}")` : `literal(${type.value})`;
35
36
  case 'union':
@@ -63,7 +64,7 @@ function printInlineObjectCompact(obj: InlineObjectTypeNode): string {
63
64
  /** Multi-line enum form — one value per line, used when single-line would exceed print width. */
64
65
  export function printEnumExpanded(values: string[], indent: string): string {
65
66
  const innerIndent = indent + INDENT;
66
- return `enum(\n${values.map(v => `${innerIndent}${v}`).join(',\n')}\n${indent})`;
67
+ return `enum(\n${values.map(v => `${innerIndent}${formatEnumValue(v)}`).join(',\n')}\n${indent})`;
67
68
  }
68
69
 
69
70
  // ─── Field printer ──────────────────────────────────────────────────────────
@@ -111,6 +112,12 @@ export function printInlineObjectExpanded(obj: InlineObjectTypeNode, indent: str
111
112
 
112
113
  // ─── Helpers ────────────────────────────────────────────────────────────────
113
114
 
115
+ /** Format a single enum value: bare identifier stays bare; anything else gets double-quoted. */
116
+ export function formatEnumValue(v: string): string {
117
+ if (/^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(v)) return v;
118
+ return `"${v}"`;
119
+ }
120
+
114
121
  /** Format a default value: quote strings that aren't valid bare identifiers. */
115
122
  export function formatDefault(val: string | number | boolean): string {
116
123
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
@@ -649,3 +649,94 @@ operation /users/{slug}: {
649
649
  expect(roundTrip(source)).toBe(source);
650
650
  });
651
651
  });
652
+
653
+ describe('printCk — options keys and services quoting', () => {
654
+ function roundTrip(source: string): string {
655
+ const diag = new DiagnosticCollector();
656
+ const ast = parseCk(source, 'test.ck', diag);
657
+ expect(diag.hasErrors()).toBe(false);
658
+ return printCk(ast);
659
+ }
660
+
661
+ it('leaves simple identifier keys values unquoted', () => {
662
+ const source = `\
663
+ options {
664
+ keys: {
665
+ area: payments
666
+ }
667
+ }
668
+ `;
669
+ expect(roundTrip(source)).toBe(source);
670
+ });
671
+
672
+ it('preserves quotes on path-like values containing slashes', () => {
673
+ const source = `\
674
+ options {
675
+ keys: {
676
+ bruno: "../../bruno/"
677
+ }
678
+ }
679
+ `;
680
+ expect(roundTrip(source)).toBe(source);
681
+ });
682
+
683
+ it('quotes values that start with a dot', () => {
684
+ const source = `\
685
+ options {
686
+ keys: {
687
+ path: "../relative"
688
+ }
689
+ }
690
+ `;
691
+ expect(roundTrip(source)).toBe(source);
692
+ });
693
+
694
+ it('preserves quotes on service paths starting with #', () => {
695
+ const source = `\
696
+ options {
697
+ services: {
698
+ AuthService: "#src/modules/auth/auth.service.js"
699
+ }
700
+ }
701
+ `;
702
+ expect(roundTrip(source)).toBe(source);
703
+ });
704
+
705
+ it('leaves plain identifier service names unquoted', () => {
706
+ const source = `\
707
+ options {
708
+ services: {
709
+ AuthService: authService
710
+ }
711
+ }
712
+ `;
713
+ expect(roundTrip(source)).toBe(source);
714
+ });
715
+ });
716
+
717
+ describe('printCk — enum values with spaces (round-trip)', () => {
718
+ function roundTrip(source: string): string {
719
+ const diag = new DiagnosticCollector();
720
+ const ast = parseCk(source, 'test.ck', diag);
721
+ expect(diag.hasErrors()).toBe(false);
722
+ return printCk(ast);
723
+ }
724
+
725
+ it('round-trips enum with bare identifiers unchanged', () => {
726
+ const source = `\
727
+ contract M: {
728
+ status: enum(active, inactive, pending)
729
+ }
730
+ `;
731
+ expect(roundTrip(source)).toBe(source);
732
+ });
733
+
734
+ it('round-trips enum with quoted multi-word values', () => {
735
+ const source = `\
736
+ contract M: {
737
+ entityType: enum("Sole Proprietorship", LLC, "Limited Partnership")
738
+ }
739
+ `;
740
+ expect(roundTrip(source)).toBe(source);
741
+ });
742
+ });