@intentius/chant-lexicon-terraform 0.57.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.
Files changed (131) hide show
  1. package/README.md +53 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +11 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +3 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate.d.ts +28 -0
  9. package/dist/codegen/generate.d.ts.map +1 -0
  10. package/dist/codegen/package.d.ts +17 -0
  11. package/dist/codegen/package.d.ts.map +1 -0
  12. package/dist/composites/terraform-apply-op.d.ts +88 -0
  13. package/dist/composites/terraform-apply-op.d.ts.map +1 -0
  14. package/dist/composites/terraform-watch-op.d.ts +124 -0
  15. package/dist/composites/terraform-watch-op.d.ts.map +1 -0
  16. package/dist/config.d.ts +83 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/describe-resources.d.ts +127 -0
  19. package/dist/describe-resources.d.ts.map +1 -0
  20. package/dist/generated/index.d.ts +2 -0
  21. package/dist/generated/index.d.ts.map +1 -0
  22. package/dist/hcl/parse.d.ts +87 -0
  23. package/dist/hcl/parse.d.ts.map +1 -0
  24. package/dist/hcl/roots.d.ts +37 -0
  25. package/dist/hcl/roots.d.ts.map +1 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/integrity.json +12 -0
  29. package/dist/lint/audit-catalog.d.ts +12 -0
  30. package/dist/lint/audit-catalog.d.ts.map +1 -0
  31. package/dist/lint/audit-lineage.d.ts +21 -0
  32. package/dist/lint/audit-lineage.d.ts.map +1 -0
  33. package/dist/lint/post-synth/index.d.ts +3 -0
  34. package/dist/lint/post-synth/index.d.ts.map +1 -0
  35. package/dist/lint/post-synth/tf001.d.ts +17 -0
  36. package/dist/lint/post-synth/tf001.d.ts.map +1 -0
  37. package/dist/lint/rules/index.d.ts +5 -0
  38. package/dist/lint/rules/index.d.ts.map +1 -0
  39. package/dist/lint/rules/plan-before-apply.d.ts +30 -0
  40. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -0
  41. package/dist/lsp/completions.d.ts +26 -0
  42. package/dist/lsp/completions.d.ts.map +1 -0
  43. package/dist/lsp/context.d.ts +70 -0
  44. package/dist/lsp/context.d.ts.map +1 -0
  45. package/dist/lsp/hover.d.ts +12 -0
  46. package/dist/lsp/hover.d.ts.map +1 -0
  47. package/dist/lsp/option-keys.d.ts +34 -0
  48. package/dist/lsp/option-keys.d.ts.map +1 -0
  49. package/dist/manifest.json +6 -0
  50. package/dist/meta.json +1 -0
  51. package/dist/okf/index.md +8 -0
  52. package/dist/okf/rules/TF001.md +11 -0
  53. package/dist/okf/rules/TF101.md +11 -0
  54. package/dist/op/activities/index.d.ts +22 -0
  55. package/dist/op/activities/index.d.ts.map +1 -0
  56. package/dist/op/activities/terraform.d.ts +213 -0
  57. package/dist/op/activities/terraform.d.ts.map +1 -0
  58. package/dist/op/builders.d.ts +56 -0
  59. package/dist/op/builders.d.ts.map +1 -0
  60. package/dist/package-cli.d.ts +3 -0
  61. package/dist/package-cli.d.ts.map +1 -0
  62. package/dist/plugin.d.ts +11 -0
  63. package/dist/plugin.d.ts.map +1 -0
  64. package/dist/rules/plan-before-apply.ts +143 -0
  65. package/dist/rules/tf001.ts +66 -0
  66. package/dist/serializer.d.ts +19 -0
  67. package/dist/serializer.d.ts.map +1 -0
  68. package/dist/skills/chant-terraform.md +92 -0
  69. package/dist/state-ownership.d.ts +27 -0
  70. package/dist/state-ownership.d.ts.map +1 -0
  71. package/dist/types/index.d.ts +2 -0
  72. package/dist/validate-cli.d.ts +3 -0
  73. package/dist/validate-cli.d.ts.map +1 -0
  74. package/dist/validate.d.ts +15 -0
  75. package/dist/validate.d.ts.map +1 -0
  76. package/package.json +75 -0
  77. package/src/__fixtures__/no-backend/main.tf +24 -0
  78. package/src/__fixtures__/show-state.json +68 -0
  79. package/src/__fixtures__/with-backend/main.tf +28 -0
  80. package/src/__fixtures__/with-module/main.tf +49 -0
  81. package/src/__fixtures__/with-module/modules/inner/main.tf +5 -0
  82. package/src/codegen/docs-cli.ts +4 -0
  83. package/src/codegen/docs.ts +50 -0
  84. package/src/codegen/generate-cli.ts +10 -0
  85. package/src/codegen/generate.ts +68 -0
  86. package/src/codegen/package.ts +50 -0
  87. package/src/composites/terraform-apply-op.acceptance.test.ts +138 -0
  88. package/src/composites/terraform-apply-op.test.ts +193 -0
  89. package/src/composites/terraform-apply-op.ts +166 -0
  90. package/src/composites/terraform-watch-op.test.ts +184 -0
  91. package/src/composites/terraform-watch-op.ts +204 -0
  92. package/src/config.ts +76 -0
  93. package/src/describe-resources.test.ts +342 -0
  94. package/src/describe-resources.ts +357 -0
  95. package/src/generated/index.d.ts +2 -0
  96. package/src/generated/index.ts +4 -0
  97. package/src/generated/lexicon-terraform.json +1 -0
  98. package/src/hcl/parse.ts +235 -0
  99. package/src/hcl/roots.ts +67 -0
  100. package/src/index.ts +65 -0
  101. package/src/lint/audit-catalog.ts +30 -0
  102. package/src/lint/audit-lineage.ts +21 -0
  103. package/src/lint/audit.test.ts +45 -0
  104. package/src/lint/post-synth/index.ts +7 -0
  105. package/src/lint/post-synth/post-synth.test.ts +95 -0
  106. package/src/lint/post-synth/tf001.ts +66 -0
  107. package/src/lint/rules/index.ts +7 -0
  108. package/src/lint/rules/plan-before-apply.test.ts +111 -0
  109. package/src/lint/rules/plan-before-apply.ts +143 -0
  110. package/src/lsp/completions.test.ts +120 -0
  111. package/src/lsp/completions.ts +101 -0
  112. package/src/lsp/context.test.ts +152 -0
  113. package/src/lsp/context.ts +349 -0
  114. package/src/lsp/hover.test.ts +82 -0
  115. package/src/lsp/hover.ts +44 -0
  116. package/src/lsp/option-keys.ts +106 -0
  117. package/src/op/activities/index.ts +48 -0
  118. package/src/op/activities/registry.test.ts +29 -0
  119. package/src/op/activities/terraform.test.ts +445 -0
  120. package/src/op/activities/terraform.ts +469 -0
  121. package/src/op/builders.test.ts +90 -0
  122. package/src/op/builders.ts +96 -0
  123. package/src/package-cli.ts +21 -0
  124. package/src/plugin.test.ts +271 -0
  125. package/src/plugin.ts +157 -0
  126. package/src/serializer.test.ts +26 -0
  127. package/src/serializer.ts +26 -0
  128. package/src/skills/chant-terraform.md +92 -0
  129. package/src/state-ownership.ts +32 -0
  130. package/src/validate-cli.ts +5 -0
  131. package/src/validate.ts +28 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * LSP completions tests (#2088). Root-name completion needs a reachable
3
+ * `chant.config.*`, so those cases write one to a temp directory rather than
4
+ * mocking `./context` — the point of the feature is reading a real file.
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import type { CompletionContext } from "@intentius/chant/lsp/types";
13
+ import { completions } from "./completions";
14
+
15
+ function ctx(overrides: Partial<CompletionContext>): CompletionContext {
16
+ return {
17
+ uri: "untitled:Untitled-1",
18
+ content: "",
19
+ position: { line: 0, character: 0 },
20
+ wordAtCursor: "",
21
+ linePrefix: "",
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe("LSP completions", () => {
27
+ it("returns an array for an empty context", () => {
28
+ expect(Array.isArray(completions(ctx({})))).toBe(true);
29
+ });
30
+
31
+ it("completes root names inside a builder's positional root argument, from a reachable config", () => {
32
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
33
+ try {
34
+ writeFileSync(
35
+ join(dir, "chant.config.ts"),
36
+ `export default { terraform: { roots: { app: { dir: "./terraform" }, db: { dir: "./db" } } } };`,
37
+ );
38
+ const uri = pathToFileURL(join(dir, "infra.op.ts")).toString();
39
+ const linePrefix = 'terraformPlan("';
40
+ const items = completions(ctx({ uri, linePrefix }));
41
+ expect(items.map((i) => i.label).sort()).toEqual(["app", "db"]);
42
+ expect(items[0].kind).toBe("value");
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ it("completes root names inside a named `root:` field", () => {
49
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
50
+ try {
51
+ writeFileSync(join(dir, "chant.config.ts"), `export default { terraform: { roots: { app: { dir: "./tf" } } } };`);
52
+ const uri = pathToFileURL(join(dir, "infra.op.ts")).toString();
53
+ const items = completions(ctx({ uri, linePrefix: ' root: "a' }));
54
+ expect(items.map((i) => i.label)).toEqual(["app"]);
55
+ } finally {
56
+ rmSync(dir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ it("filters root names by the prefix already typed", () => {
61
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
62
+ try {
63
+ writeFileSync(
64
+ join(dir, "chant.config.ts"),
65
+ `export default { terraform: { roots: { app: { dir: "./a" }, api: { dir: "./b" }, db: { dir: "./c" } } } };`,
66
+ );
67
+ const uri = pathToFileURL(join(dir, "infra.op.ts")).toString();
68
+ const items = completions(ctx({ uri, linePrefix: 'terraformApply("ap' }));
69
+ expect(items.map((i) => i.label).sort()).toEqual(["api", "app"]);
70
+ } finally {
71
+ rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ it("offers no root names when no chant.config.* is reachable, rather than fabricating any", () => {
76
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
77
+ try {
78
+ const uri = pathToFileURL(join(dir, "infra.op.ts")).toString();
79
+ const items = completions(ctx({ uri, linePrefix: 'terraformPlan("' }));
80
+ expect(items).toEqual([]);
81
+ } finally {
82
+ rmSync(dir, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ it("completes TerraformApplyOp's own config keys inside its argument object", () => {
87
+ const content = 'export const { op } = TerraformApplyOp({\n name: "app-apply",\n ';
88
+ const items = completions(
89
+ ctx({ content, position: { line: 2, character: 2 }, uri: "untitled:Untitled-1" }),
90
+ );
91
+ const labels = items.map((i) => i.label);
92
+ expect(labels).toContain("gate");
93
+ expect(labels).toContain("compensate");
94
+ expect(labels).toContain("taskQueue");
95
+ });
96
+
97
+ it("completes a builder's own opts keys inside its second argument", () => {
98
+ const content = 'terraformPlan("app", {\n ';
99
+ const items = completions(ctx({ content, position: { line: 1, character: 2 } }));
100
+ expect(items.map((i) => i.label)).toEqual(expect.arrayContaining(["planFile", "destroy", "cwd", "id"]));
101
+ });
102
+
103
+ it("completes root-entry keys inside chant.config.ts's terraform.roots.<name>", () => {
104
+ const content = "export default {\n terraform: {\n roots: {\n app: {\n ";
105
+ const items = completions(ctx({ content, position: { line: 4, character: 8 } }));
106
+ expect(items.map((i) => i.label).sort()).toEqual(["backendConfig", "dir", "varFiles", "workspace"]);
107
+ });
108
+
109
+ it("completes the terraform namespace's own top-level keys", () => {
110
+ const content = "export default {\n terraform: {\n ";
111
+ const items = completions(ctx({ content, position: { line: 1, character: 4 } }));
112
+ expect(items.map((i) => i.label).sort()).toEqual(["binary", "roots"]);
113
+ });
114
+
115
+ it("completes compensate's own key", () => {
116
+ const content = 'TerraformApplyOp({\n compensate: {\n ';
117
+ const items = completions(ctx({ content, position: { line: 2, character: 4 } }));
118
+ expect(items.map((i) => i.label)).toEqual(["command"]);
119
+ });
120
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * LSP completions for the terraform lexicon (#2088).
3
+ *
4
+ * k3s's shape (a generated `LexiconIndex` over typed resource classes) does
5
+ * not fit here: terraform's declarables are lightweight per-HCL-block
6
+ * entities `buildRoots()` produces at build time, not authored TypeScript
7
+ * types — there is nothing to generate a completion index from. What an
8
+ * ops-only lexicon can honestly complete is the surface an `*.op.ts` author
9
+ * actually types:
10
+ *
11
+ * - `terraform.roots` names, read from the reachable `chant.config.*` (see
12
+ * `./context.ts`) — completed inside a root-name position, e.g.
13
+ * `terraformPlan("` or `root: "`.
14
+ * - the composite/builder option keys (`./option-keys.ts`) — completed
15
+ * inside the relevant call's options object.
16
+ *
17
+ * Root names complete only when a `chant.config.*` is reachable from the
18
+ * document's own `uri` and declares at least one; there is no synchronous,
19
+ * safe way to evaluate `chant.config.ts` (project-authored code) from inside
20
+ * a completion request, so an unreachable config yields no root-name
21
+ * completions rather than a guessed one. Option keys need no config at all
22
+ * and complete unconditionally.
23
+ */
24
+
25
+ import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
26
+ import { contentUpToCursor, enclosingCallName, enclosingKeyPath, innermostObjectKey, resolveRootsForUri } from "./context";
27
+ import { APPLY_OP_KEYS, BUILDER_OPTS_BY_CALL, COMPENSATE_KEYS, CONFIG_NAMESPACE_KEYS, ROOT_ENTRY_KEYS, type OptionKey } from "./option-keys";
28
+
29
+ /** `terraformPlan("` / `terraformApply(` — the first positional string arg, which every builder types as `root`. */
30
+ const ROOT_POSITIONAL_ARG = new RegExp(`\\b(${Object.keys(BUILDER_OPTS_BY_CALL).join("|")})\\(\\s*["'](\\w*)$`);
31
+
32
+ /** `root: "` — the named field, in `TerraformApplyOp({ ... })` or a raw `activity("terraformPlan", { root: "..." })`. */
33
+ const ROOT_NAMED_FIELD = /\broot\s*:\s*["'](\w*)$/;
34
+
35
+ function keyCompletions(keys: OptionKey[], prefix: string): CompletionItem[] {
36
+ const lower = prefix.toLowerCase();
37
+ return keys
38
+ .filter((k) => !lower || k.key.toLowerCase().startsWith(lower))
39
+ .map((k) => ({
40
+ label: k.key,
41
+ insertText: k.key,
42
+ kind: "property" as const,
43
+ documentation: k.detail,
44
+ }));
45
+ }
46
+
47
+ function rootCompletions(uri: string, prefix: string): CompletionItem[] {
48
+ const roots = resolveRootsForUri(uri);
49
+ if (!roots) return [];
50
+ const lower = prefix.toLowerCase();
51
+ return Object.entries(roots)
52
+ .filter(([name]) => !lower || name.toLowerCase().startsWith(lower))
53
+ .sort(([a], [b]) => a.localeCompare(b))
54
+ .map(([name, root]) => ({
55
+ label: name,
56
+ insertText: name,
57
+ kind: "value" as const,
58
+ detail: root.dir,
59
+ documentation: [
60
+ root.dir ? `dir: ${root.dir}` : undefined,
61
+ root.workspace ? `workspace: ${root.workspace}` : undefined,
62
+ root.varFiles?.length ? `varFiles: ${root.varFiles.join(", ")}` : undefined,
63
+ ]
64
+ .filter((line): line is string => Boolean(line))
65
+ .join("\n"),
66
+ }));
67
+ }
68
+
69
+ export function completions(ctx: CompletionContext): CompletionItem[] {
70
+ const linePrefix = ctx.linePrefix ?? "";
71
+
72
+ const positional = ROOT_POSITIONAL_ARG.exec(linePrefix);
73
+ if (positional) return rootCompletions(ctx.uri, positional[2]);
74
+
75
+ const namedField = ROOT_NAMED_FIELD.exec(linePrefix);
76
+ if (namedField) return rootCompletions(ctx.uri, namedField[1]);
77
+
78
+ const upToCursor = contentUpToCursor(ctx.content ?? "", ctx.position?.line ?? 0);
79
+ const prefix = ctx.wordAtCursor ?? "";
80
+
81
+ const call = enclosingCallName(upToCursor);
82
+ if (call === "TerraformApplyOp") return keyCompletions(APPLY_OP_KEYS, prefix);
83
+ if (call && call in BUILDER_OPTS_BY_CALL) return keyCompletions(BUILDER_OPTS_BY_CALL[call], prefix);
84
+
85
+ // `compensate: { <cursor>` — checked independent of the call chain above,
86
+ // since `compensate` itself sits inside `TerraformApplyOp({ ... })`'s own
87
+ // argument object, which `enclosingKeyPath` below refuses to walk through.
88
+ if (innermostObjectKey(upToCursor) === "compensate") return keyCompletions(COMPENSATE_KEYS, prefix);
89
+
90
+ // The remaining contexts are all plain nested object literals with no call
91
+ // in the chain — `chant.config.ts`'s own `terraform: { ... }` namespace.
92
+ const path = enclosingKeyPath(upToCursor);
93
+ if (path) {
94
+ const last = path[path.length - 1];
95
+ const parent = path[path.length - 2];
96
+ if (parent === "roots") return keyCompletions(ROOT_ENTRY_KEYS, prefix);
97
+ if (last === "terraform") return keyCompletions(CONFIG_NAMESPACE_KEYS, prefix);
98
+ }
99
+
100
+ return [];
101
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Tests for the shared LSP context helpers (#2088): parsing `terraform.roots`
3
+ * out of a `chant.config.ts`/`.json`'s raw text (never executing it), and the
4
+ * call/field detectors `completions.ts` uses to place the cursor.
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import {
13
+ contentUpToCursor,
14
+ enclosingCallName,
15
+ enclosingKeyPath,
16
+ innermostObjectKey,
17
+ parseTerraformRoots,
18
+ resolveRootsForUri,
19
+ } from "./context";
20
+
21
+ describe("parseTerraformRoots (.ts)", () => {
22
+ it("extracts dir, workspace and varFiles per root", () => {
23
+ const text = `
24
+ import type { ChantConfig } from "@intentius/chant/config";
25
+ import "@intentius/chant-lexicon-terraform";
26
+
27
+ export default {
28
+ lexicons: ["terraform"],
29
+ terraform: {
30
+ binary: "tofu",
31
+ roots: {
32
+ app: { dir: "./terraform/app", workspace: "prod", varFiles: ["prod.tfvars", "shared.tfvars"] },
33
+ db: {
34
+ dir: "./terraform/db",
35
+ },
36
+ },
37
+ },
38
+ } satisfies ChantConfig;
39
+ `;
40
+ const roots = parseTerraformRoots(text, "ts");
41
+ expect(roots).toEqual({
42
+ app: { dir: "./terraform/app", workspace: "prod", varFiles: ["prod.tfvars", "shared.tfvars"] },
43
+ db: { dir: "./terraform/db", workspace: undefined, varFiles: undefined },
44
+ });
45
+ });
46
+
47
+ it("returns undefined when the config declares no terraform namespace", () => {
48
+ expect(parseTerraformRoots(`export default { lexicons: ["k3s"] };`, "ts")).toBeUndefined();
49
+ });
50
+
51
+ it("returns undefined when terraform.roots is empty", () => {
52
+ expect(parseTerraformRoots(`export default { terraform: { roots: {} } };`, "ts")).toBeUndefined();
53
+ });
54
+ });
55
+
56
+ describe("parseTerraformRoots (.json)", () => {
57
+ it("reads a plain object, no brace-matching needed", () => {
58
+ const json = JSON.stringify({ terraform: { roots: { app: { dir: "./tf" } } } });
59
+ expect(parseTerraformRoots(json, "json")).toEqual({
60
+ app: { dir: "./tf", workspace: undefined, varFiles: undefined },
61
+ });
62
+ });
63
+
64
+ it("returns undefined for invalid JSON rather than throwing", () => {
65
+ expect(parseTerraformRoots("{ not json", "json")).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe("resolveRootsForUri", () => {
70
+ it("walks up from the document's directory to the nearest chant.config.ts", () => {
71
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
72
+ try {
73
+ writeFileSync(
74
+ join(dir, "chant.config.ts"),
75
+ `export default { terraform: { roots: { app: { dir: "./tf" } } } };`,
76
+ );
77
+ const nested = join(dir, "src");
78
+ const uri = pathToFileURL(join(nested, "infra.op.ts")).toString();
79
+ expect(resolveRootsForUri(uri)).toEqual({ app: { dir: "./tf", workspace: undefined, varFiles: undefined } });
80
+ } finally {
81
+ rmSync(dir, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ it("returns undefined for a non-file:// uri", () => {
86
+ expect(resolveRootsForUri("untitled:Untitled-1")).toBeUndefined();
87
+ });
88
+
89
+ it("returns undefined when no chant.config.* is reachable", () => {
90
+ const dir = mkdtempSync(join(tmpdir(), "chant-terraform-lsp-"));
91
+ try {
92
+ const uri = pathToFileURL(join(dir, "infra.op.ts")).toString();
93
+ expect(resolveRootsForUri(uri)).toBeUndefined();
94
+ } finally {
95
+ rmSync(dir, { recursive: true, force: true });
96
+ }
97
+ });
98
+ });
99
+
100
+ describe("enclosingCallName", () => {
101
+ it("finds the call whose sole argument object holds the cursor", () => {
102
+ const text = 'export const { op } = TerraformApplyOp({\n name: "x",\n ';
103
+ expect(enclosingCallName(text)).toBe("TerraformApplyOp");
104
+ });
105
+
106
+ it("finds the call when the object is a later positional argument", () => {
107
+ const text = 'terraformPlan("app", {\n ';
108
+ expect(enclosingCallName(text)).toBe("terraformPlan");
109
+ });
110
+
111
+ it("returns undefined outside any call", () => {
112
+ expect(enclosingCallName("const x = {\n ")).toBeUndefined();
113
+ });
114
+
115
+ it("returns undefined when the cursor is not inside an object literal at all", () => {
116
+ expect(enclosingCallName("terraformPlan(")).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe("enclosingKeyPath", () => {
121
+ it("names the chain of enclosing object keys", () => {
122
+ const text = "export default {\n terraform: {\n roots: {\n app: {\n ";
123
+ expect(enclosingKeyPath(text)).toEqual(["terraform", "roots", "app"]);
124
+ });
125
+
126
+ it("returns undefined once a call sits in the chain", () => {
127
+ const text = "foo({\n bar: {\n ";
128
+ expect(enclosingKeyPath(text)).toBeUndefined();
129
+ });
130
+
131
+ it("contributes nothing for the top-level object literal, which no key precedes", () => {
132
+ const text = "export default {\n ";
133
+ expect(enclosingKeyPath(text)).toEqual([]);
134
+ });
135
+ });
136
+
137
+ describe("innermostObjectKey", () => {
138
+ it("names the immediate enclosing key even when a call sits further out", () => {
139
+ const text = "TerraformApplyOp({\n compensate: {\n ";
140
+ expect(innermostObjectKey(text)).toBe("compensate");
141
+ });
142
+
143
+ it("returns undefined when the cursor is not inside any object literal", () => {
144
+ expect(innermostObjectKey("terraformPlan(")).toBeUndefined();
145
+ });
146
+ });
147
+
148
+ describe("contentUpToCursor", () => {
149
+ it("joins every line through the cursor's own line", () => {
150
+ expect(contentUpToCursor("a\nb\nc", 1)).toBe("a\nb");
151
+ });
152
+ });