Skip to content

Commit 1fa795e

Browse files
committed
feat: TF-25532: Recognize tfcomponent.hcl extension
1 parent 88ed5a6 commit 1fa795e

26 files changed

+852
-5
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ HCL syntax is checked for e.g. missing control characters like `}`, `"` or other
7070

7171
![](docs/validation-rule-hcl.png)
7272

73-
Enhanced validation of selected Terraform language constructs in both `*.tf` and `*.tfvars` files based on detected Terraform version and provider versions is also provided. This also works for Terraform Stacks language constructs in both `*.tfstack.hcl` and `*.tfdeploy.hcl` files.
73+
Enhanced validation of selected Terraform language constructs in both `*.tf` and `*.tfvars` files based on detected Terraform version and provider versions is also provided. This also works for Terraform Stacks language constructs in `*.tfcomponent.hcl`, `*.tfstack.hcl` and `*.tfdeploy.hcl` files.
7474

7575
This can highlight deprecations, missing required attributes or blocks, references to undeclared variables and more, [as documented](https://github.com/hashicorp/terraform-ls/blob/main/docs/validation.md#enhanced-validation).
7676

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"Terraform Stack"
8282
],
8383
"extensions": [
84-
".tfstack.hcl"
84+
".tfstack.hcl",
85+
".tfcomponent.hcl"
8586
],
8687
"configuration": "./language-configuration.json",
8788
"icon": {

src/extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
9090
vscode.workspace.createFileSystemWatcher('**/*.tf'),
9191
vscode.workspace.createFileSystemWatcher('**/*.tfvars'),
9292
vscode.workspace.createFileSystemWatcher('**/*.tfstack.hcl'),
93+
vscode.workspace.createFileSystemWatcher('**/*.tfcomponent.hcl'),
9394
vscode.workspace.createFileSystemWatcher('**/*.tfdeploy.hcl'),
9495
vscode.workspace.createFileSystemWatcher('**/*.tftest.hcl'),
9596
vscode.workspace.createFileSystemWatcher('**/*.tfmock.hcl'),
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import * as vscode from 'vscode';
7+
import { assert } from 'chai';
8+
import { activateExtension, getDocUri, open, testCompletion } from '../../helper';
9+
10+
suite('stacks deployments', () => {
11+
suite('basics', function () {
12+
const docUri = getDocUri('deployments.tfdeploy.hcl');
13+
14+
this.beforeAll(async () => {
15+
await open(docUri);
16+
await activateExtension();
17+
});
18+
19+
this.afterAll(async () => {
20+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
21+
});
22+
23+
this.afterEach(async () => {
24+
// revert any changes made to the document after each test
25+
await vscode.commands.executeCommand('workbench.action.files.revert');
26+
});
27+
28+
test('language is registered', async () => {
29+
const doc = await vscode.workspace.openTextDocument(docUri);
30+
assert.equal(doc.languageId, 'terraform-deploy', 'document language should be `terraform-deploy`');
31+
});
32+
33+
test('completes inputs attribute in deployment block', async () => {
34+
// add a new incomplete "test" deployment block to use for completions
35+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
36+
editBuilder.insert(
37+
new vscode.Position(14, 0),
38+
`
39+
deployment "test" {
40+
41+
}
42+
`,
43+
);
44+
});
45+
46+
const expected = [new vscode.CompletionItem('inputs', vscode.CompletionItemKind.Property)];
47+
48+
await testCompletion(docUri, new vscode.Position(16, 2), {
49+
items: expected,
50+
});
51+
});
52+
53+
test('completes available inputs in deployment block', async () => {
54+
// add a new incomplete "test" deployment block to use for completions
55+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
56+
editBuilder.insert(
57+
new vscode.Position(14, 0),
58+
`
59+
deployment "test" {
60+
inputs = {
61+
62+
}
63+
}
64+
`,
65+
);
66+
});
67+
68+
const expected = [
69+
new vscode.CompletionItem('default_tags', vscode.CompletionItemKind.Property),
70+
new vscode.CompletionItem('identity_token_file', vscode.CompletionItemKind.Property),
71+
new vscode.CompletionItem('region', vscode.CompletionItemKind.Property),
72+
new vscode.CompletionItem('role_arn', vscode.CompletionItemKind.Property),
73+
];
74+
75+
await testCompletion(docUri, new vscode.Position(17, 2), {
76+
items: expected,
77+
});
78+
});
79+
80+
test('completes attributes of identity_token block', async () => {
81+
// add a new incomplete deployment block to use for completions
82+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
83+
editBuilder.insert(
84+
new vscode.Position(14, 0),
85+
`
86+
deployment "test" {
87+
inputs = {
88+
identity_token_file = identity_token.aws.
89+
}
90+
}
91+
`,
92+
);
93+
});
94+
95+
const expected = [
96+
new vscode.CompletionItem('identity_token.aws.audience', vscode.CompletionItemKind.Variable),
97+
new vscode.CompletionItem('identity_token.aws.jwt', vscode.CompletionItemKind.Variable),
98+
new vscode.CompletionItem('identity_token.aws.jwt_filename', vscode.CompletionItemKind.Variable),
99+
];
100+
101+
await testCompletion(docUri, new vscode.Position(17, 45), {
102+
items: expected,
103+
});
104+
});
105+
106+
test('completes audience in identity_token block', async () => {
107+
// add a new incomplete "account_3" identity_token block to use for completions
108+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
109+
editBuilder.insert(new vscode.Position(14, 0), 'identity_token "test" {\n\n}\n');
110+
});
111+
112+
const expected = [new vscode.CompletionItem('audience', vscode.CompletionItemKind.Property)];
113+
114+
await testCompletion(docUri, new vscode.Position(15, 2), {
115+
items: expected,
116+
});
117+
});
118+
119+
// TODO: not implemented yet
120+
test.skip('completes valid rule types of an orchestrate block', async () => {
121+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
122+
editBuilder.insert(new vscode.Position(14, 0), 'orchestrate ""\n\n');
123+
});
124+
125+
const expected = [new vscode.CompletionItem('auto_approve', vscode.CompletionItemKind.Property)];
126+
127+
await testCompletion(docUri, new vscode.Position(14, 13), {
128+
items: expected,
129+
});
130+
});
131+
132+
suite('orchestrate block context', function () {
133+
this.beforeAll(async () => {
134+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
135+
editBuilder.insert(
136+
new vscode.Position(14, 0),
137+
`
138+
orchestrate "auto_approve" "no_api_gateway_changes" {
139+
check {
140+
condition = context.plan.component_changes["component.api_gateway"].total == 0
141+
reason = "Changes proposed to api_gateway component."
142+
}
143+
}
144+
`,
145+
);
146+
});
147+
});
148+
149+
test.skip('completes context', async () => {
150+
const generateSubChanges = (label: string) => [
151+
new vscode.CompletionItem(label, vscode.CompletionItemKind.Variable),
152+
new vscode.CompletionItem(`${label}.add`, vscode.CompletionItemKind.Variable),
153+
new vscode.CompletionItem(`${label}.change`, vscode.CompletionItemKind.Variable),
154+
new vscode.CompletionItem(`${label}.defer`, vscode.CompletionItemKind.Variable),
155+
new vscode.CompletionItem(`${label}.forget`, vscode.CompletionItemKind.Variable),
156+
new vscode.CompletionItem(`${label}.import`, vscode.CompletionItemKind.Variable),
157+
new vscode.CompletionItem(`${label}.move`, vscode.CompletionItemKind.Variable),
158+
new vscode.CompletionItem(`${label}.remove`, vscode.CompletionItemKind.Variable),
159+
new vscode.CompletionItem(`${label}.total`, vscode.CompletionItemKind.Variable),
160+
];
161+
162+
const expected = [
163+
new vscode.CompletionItem('context.errors', vscode.CompletionItemKind.Variable),
164+
new vscode.CompletionItem('context.operation', vscode.CompletionItemKind.Variable),
165+
new vscode.CompletionItem('context.plan', vscode.CompletionItemKind.Variable),
166+
new vscode.CompletionItem('context.plan.applyable', vscode.CompletionItemKind.Variable),
167+
...generateSubChanges('context.plan.changes'),
168+
...generateSubChanges('context.plan.component_changes["api_gateway"]'),
169+
...generateSubChanges('context.plan.component_changes["foo"]'),
170+
...generateSubChanges('context.plan.component_changes["lambda"]'),
171+
...generateSubChanges('context.plan.component_changes["s3"]'),
172+
new vscode.CompletionItem('context.plan.deployment', vscode.CompletionItemKind.Variable),
173+
new vscode.CompletionItem('context.plan.mode', vscode.CompletionItemKind.Variable),
174+
new vscode.CompletionItem('context.plan.replans', vscode.CompletionItemKind.Variable),
175+
new vscode.CompletionItem('context.success', vscode.CompletionItemKind.Variable),
176+
new vscode.CompletionItem('context.warnings', vscode.CompletionItemKind.Variable),
177+
];
178+
179+
await testCompletion(docUri, new vscode.Position(17, 26), {
180+
items: expected,
181+
});
182+
});
183+
});
184+
});
185+
});
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import * as vscode from 'vscode';
7+
import { assert } from 'chai';
8+
import { activateExtension, getDocUri, open, testCompletion } from '../../helper';
9+
10+
suite('stacks stack - legacy extension', () => {
11+
suite('root', function suite() {
12+
const docUri = getDocUri('variables.tfstack.hcl');
13+
14+
this.beforeAll(async () => {
15+
await open(docUri);
16+
await activateExtension();
17+
});
18+
19+
this.afterAll(async () => {
20+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
21+
});
22+
23+
test('language is registered', async () => {
24+
const doc = await vscode.workspace.openTextDocument(docUri);
25+
assert.equal(doc.languageId, 'terraform-stack', 'document language should be `terraform-stack`');
26+
});
27+
28+
test('completes blocks available for stacks files', async () => {
29+
const expected = [
30+
new vscode.CompletionItem('component', vscode.CompletionItemKind.Class),
31+
new vscode.CompletionItem('locals', vscode.CompletionItemKind.Class),
32+
new vscode.CompletionItem('output', vscode.CompletionItemKind.Class),
33+
new vscode.CompletionItem('provider', vscode.CompletionItemKind.Class),
34+
new vscode.CompletionItem('removed', vscode.CompletionItemKind.Class),
35+
new vscode.CompletionItem('required_providers', vscode.CompletionItemKind.Class),
36+
new vscode.CompletionItem('variable', vscode.CompletionItemKind.Class),
37+
];
38+
39+
await testCompletion(docUri, new vscode.Position(20, 0), {
40+
items: expected,
41+
});
42+
});
43+
});
44+
45+
suite('components', function suite() {
46+
const docUri = getDocUri('components.tfstack.hcl');
47+
48+
this.beforeAll(async () => {
49+
await open(docUri);
50+
await activateExtension();
51+
});
52+
53+
this.afterAll(async () => {
54+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
55+
});
56+
57+
this.afterEach(async () => {
58+
// revert any changes made to the document after each test
59+
await vscode.commands.executeCommand('workbench.action.files.revert');
60+
});
61+
62+
test('language is registered', async () => {
63+
const doc = await vscode.workspace.openTextDocument(docUri);
64+
assert.equal(doc.languageId, 'terraform-stack', 'document language should be `terraform-stack`');
65+
});
66+
67+
test('completes attributes of component block', async () => {
68+
const expected = [
69+
new vscode.CompletionItem('depends_on', vscode.CompletionItemKind.Property),
70+
new vscode.CompletionItem('for_each', vscode.CompletionItemKind.Property),
71+
new vscode.CompletionItem('inputs', vscode.CompletionItemKind.Property),
72+
new vscode.CompletionItem('providers', vscode.CompletionItemKind.Property),
73+
new vscode.CompletionItem('source', vscode.CompletionItemKind.Property),
74+
new vscode.CompletionItem('version', vscode.CompletionItemKind.Property),
75+
];
76+
77+
await testCompletion(docUri, new vscode.Position(4, 12), {
78+
items: expected,
79+
});
80+
});
81+
82+
test('completes inputs for local component', async () => {
83+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
84+
editBuilder.insert(
85+
new vscode.Position(2, 0),
86+
`
87+
component "test" {
88+
source = "./lambda"
89+
90+
inputs = {
91+
92+
}
93+
}
94+
`,
95+
);
96+
});
97+
98+
const expected = [new vscode.CompletionItem('bucket_id', vscode.CompletionItemKind.Property)];
99+
100+
await testCompletion(docUri, new vscode.Position(7, 4), {
101+
items: expected,
102+
});
103+
});
104+
105+
test('completes providers for local component', async () => {
106+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
107+
editBuilder.insert(
108+
new vscode.Position(2, 0),
109+
`
110+
component "test" {
111+
source = "./lambda"
112+
113+
providers = {
114+
115+
}
116+
}
117+
`,
118+
);
119+
});
120+
121+
const expected = [
122+
new vscode.CompletionItem('archive', vscode.CompletionItemKind.Property),
123+
new vscode.CompletionItem('aws', vscode.CompletionItemKind.Property),
124+
new vscode.CompletionItem('local', vscode.CompletionItemKind.Property),
125+
new vscode.CompletionItem('random', vscode.CompletionItemKind.Property),
126+
];
127+
128+
await testCompletion(docUri, new vscode.Position(7, 4), {
129+
items: expected,
130+
});
131+
});
132+
133+
test('completes references to providers', async () => {
134+
await vscode.window.activeTextEditor?.edit((editBuilder) => {
135+
editBuilder.insert(
136+
new vscode.Position(2, 0),
137+
`
138+
component "test" {
139+
source = "./lambda"
140+
141+
providers = {
142+
aws = provider.
143+
}
144+
}
145+
`,
146+
);
147+
});
148+
149+
const expected = [
150+
new vscode.CompletionItem('provider.archive.this', vscode.CompletionItemKind.Variable),
151+
new vscode.CompletionItem('provider.aws.this', vscode.CompletionItemKind.Variable),
152+
new vscode.CompletionItem('provider.local.this', vscode.CompletionItemKind.Variable),
153+
new vscode.CompletionItem('provider.random.this', vscode.CompletionItemKind.Variable),
154+
];
155+
await testCompletion(docUri, new vscode.Position(7, 19), {
156+
items: expected,
157+
});
158+
});
159+
});
160+
161+
suite('providers', function suite() {
162+
const docUri = getDocUri('providers.tfstack.hcl');
163+
164+
this.beforeAll(async () => {
165+
await open(docUri);
166+
await activateExtension();
167+
});
168+
169+
this.afterAll(async () => {
170+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
171+
});
172+
173+
test('completes config and for_each blocks within provider', async () => {
174+
const expected = [
175+
new vscode.CompletionItem('config', vscode.CompletionItemKind.Class),
176+
new vscode.CompletionItem('for_each', vscode.CompletionItemKind.Property),
177+
];
178+
179+
await testCompletion(docUri, new vscode.Position(41, 0), {
180+
items: expected,
181+
});
182+
});
183+
});
184+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.10.0-alpha20240606

0 commit comments

Comments
 (0)