Skip to content

Commit fc4ba57

Browse files
committed
Merge branch 'hw-ff' into crash
2 parents 008e1d0 + 5d30744 commit fc4ba57

File tree

47 files changed

+1240
-296
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1240
-296
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ The server implements the Language Server Protocol (LSP) to enable code editors
4949
- **Artifact Export** - Export and upload template artifacts to S3 for deployment
5050

5151
### Code Lens Actions
52-
- **Validate Deployment** - Validate template deployment from the editor
53-
- **Deploy Template** - Deploy templates with a single click
52+
- **Validate and Deploy** - Validate and deploy templates with a single click
5453
- **Open Stack Template** - Open managed stack templates for resources
5554

5655
### Advanced Capabilities

src/artifactexporter/ArtifactExporter.ts

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,13 @@
1-
import { readFileSync } from 'fs';
21
import { resolve, dirname, isAbsolute } from 'path';
3-
import { fileURLToPath, pathToFileURL } from 'url';
4-
import { load } from 'js-yaml';
5-
import { TopLevelSection, IntrinsicFunction } from '../context/ContextType';
6-
import { Document, DocumentType } from '../document/Document';
7-
import { detectDocumentType } from '../document/DocumentUtils';
2+
import { fileURLToPath } from 'url';
3+
import { TopLevelSection } from '../context/ContextType';
4+
import { normalizeIntrinsicFunctionAndCondition } from '../context/semantic/Intrinsics';
5+
import { DocumentType } from '../document/Document';
6+
import { parseDocumentContent } from '../document/DocumentUtils';
87
import { S3Service } from '../services/S3Service';
98
import { Artifact } from '../stacks/actions/StackActionRequestType';
109
import { isS3Url, RESOURCE_EXPORTER_MAP } from './ResourceExporters';
1110

12-
const INTRINSIC_FUNCTION_MAP = new Map<string, string>([
13-
['!Ref', IntrinsicFunction.Ref],
14-
['!GetAtt', IntrinsicFunction.GetAtt],
15-
['!Join', IntrinsicFunction.Join],
16-
['!Sub', IntrinsicFunction.Sub],
17-
['!Base64', IntrinsicFunction.Base64],
18-
['!GetAZs', IntrinsicFunction.GetAZs],
19-
['!ImportValue', IntrinsicFunction.ImportValue],
20-
['!Select', IntrinsicFunction.Select],
21-
['!Split', IntrinsicFunction.Split],
22-
['!FindInMap', IntrinsicFunction.FindInMap],
23-
['!Equals', IntrinsicFunction.Equals],
24-
['!If', IntrinsicFunction.If],
25-
['!Not', IntrinsicFunction.Not],
26-
['!And', IntrinsicFunction.And],
27-
['!Or', IntrinsicFunction.Or],
28-
['!Cidr', IntrinsicFunction.Cidr],
29-
['!Transform', IntrinsicFunction.Transform],
30-
['!Condition', 'Condition'],
31-
]);
32-
3311
export type ArtifactWithProperty = {
3412
resourceType: string;
3513
resourcePropertyDict: Record<string, unknown>;
@@ -39,30 +17,14 @@ export type ArtifactWithProperty = {
3917

4018
export class ArtifactExporter {
4119
private readonly templateDict: unknown;
42-
private readonly templateUri: string;
43-
private readonly templateType: DocumentType;
4420

4521
constructor(
4622
private readonly s3Service: S3Service,
47-
private readonly document?: Document,
48-
private readonly templateAbsPath?: string,
23+
private readonly templateType: DocumentType,
24+
private readonly templateUri: string,
25+
templateContent: string,
4926
) {
50-
if (this.document) {
51-
this.templateDict = this.document.getParsedDocumentContent();
52-
this.templateUri = this.document.uri;
53-
this.templateType = this.document.documentType;
54-
} else if (this.templateAbsPath) {
55-
const content = readFileSync(this.templateAbsPath, 'utf8');
56-
this.templateUri = pathToFileURL(this.templateAbsPath).href;
57-
this.templateType = detectDocumentType(this.templateUri, content).type;
58-
if (this.templateType === DocumentType.YAML) {
59-
this.templateDict = load(content);
60-
} else {
61-
this.templateDict = JSON.parse(content);
62-
}
63-
} else {
64-
throw new Error('Either document or absolutePath must be provided');
65-
}
27+
this.templateDict = parseDocumentContent(templateUri, templateContent);
6628
}
6729

6830
private getResourceMapWithArtifact(): Record<string, ArtifactWithProperty[]> {
@@ -148,7 +110,7 @@ export class ArtifactExporter {
148110
const objDict = obj as Record<string, unknown>;
149111

150112
for (const [key, value] of Object.entries(objDict)) {
151-
const newKey = INTRINSIC_FUNCTION_MAP.get(key) ?? key;
113+
const newKey = normalizeIntrinsicFunctionAndCondition(key);
152114
result[newKey] = this.convertIntrinsicFunctionKeys(value);
153115
}
154116

src/artifactexporter/ResourceExporters.ts

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
1-
import {
2-
existsSync,
3-
mkdtempSync,
4-
copyFileSync,
5-
rmSync,
6-
createWriteStream,
7-
statSync,
8-
openSync,
9-
readSync,
10-
closeSync,
11-
} from 'fs';
1+
import { existsSync, mkdtempSync, copyFileSync, rmSync, createWriteStream, statSync, readFileSync } from 'fs';
122
import { tmpdir } from 'os';
133
import path, { join, basename } from 'path';
4+
import { pathToFileURL } from 'url';
145
import archiver from 'archiver';
156
import { dump } from 'js-yaml';
7+
import { detectDocumentType } from '../document/DocumentUtils';
168
import { S3Service } from '../services/S3Service';
179
import { ArtifactExporter } from './ArtifactExporter';
1810

1911
export function isS3Url(url: string): boolean {
20-
return typeof url === 'string' && /^s3:\/\/[^/]+\/.+/.test(url);
12+
return /^s3:\/\/[^/]+\/.+/.test(url);
2113
}
2214

2315
export function isLocalFile(filePath: string): boolean {
@@ -31,26 +23,9 @@ function isLocalFolder(path: string): boolean {
3123
function isArchiveFile(filePath: string) {
3224
// Quick extension check
3325
const ext = path.extname(filePath).toLowerCase();
34-
const archiveExts = ['.zip', '.rar', '.7z', '.tar', '.gz', '.tgz'];
35-
36-
if (!archiveExts.includes(ext)) return false;
37-
38-
// Verify with magic numbers
39-
try {
40-
const fd = openSync(filePath, 'r');
41-
const buffer = Buffer.alloc(8);
42-
readSync(fd, buffer, 0, 8, 0);
43-
closeSync(fd);
44-
45-
return (
46-
(buffer[0] === 0x50 && buffer[1] === 0x4b) || // ZIP
47-
buffer.toString('ascii', 0, 4) === 'Rar!' || // RAR
48-
(buffer[0] === 0x37 && buffer[1] === 0x7a) || // 7Z
49-
(buffer[0] === 0x1f && buffer[1] === 0x8b) // GZIP
50-
);
51-
} catch {
52-
return false;
53-
}
26+
const archiveExts = ['.zip', '.rar', '.7z', '.tar', '.gz', '.tgz', '.zst', '.war'];
27+
28+
return archiveExts.includes(ext);
5429
}
5530

5631
function copyToTempDir(filePath: string): string {
@@ -228,25 +203,25 @@ export abstract class ResourceWithS3UrlDict extends Resource {
228203
}
229204
}
230205

231-
export class ServerlessFunctionResource extends Resource {
206+
class ServerlessFunctionResource extends Resource {
232207
public override resourceType = 'AWS::Serverless::Function';
233208
public override propertyName = 'CodeUri';
234209
protected override forceZip = true;
235210
}
236211

237-
export class ServerlessApiResource extends Resource {
212+
class ServerlessApiResource extends Resource {
238213
public override resourceType = 'AWS::Serverless::Api';
239214
public override propertyName = 'DefinitionUri';
240215
protected override packageNullProperty = false;
241216
}
242217

243-
export class GraphQLSchemaResource extends Resource {
218+
class GraphQLSchemaResource extends Resource {
244219
public override resourceType = 'AWS::AppSync::GraphQLSchema';
245220
public override propertyName = 'DefinitionS3Location';
246221
protected override packageNullProperty = false;
247222
}
248223

249-
export class LambdaFunctionResource extends ResourceWithS3UrlDict {
224+
class LambdaFunctionResource extends ResourceWithS3UrlDict {
250225
public override resourceType = 'AWS::Lambda::Function';
251226
public override propertyName = 'Code';
252227
protected override bucketNameProperty = 'S3Bucket';
@@ -255,7 +230,7 @@ export class LambdaFunctionResource extends ResourceWithS3UrlDict {
255230
protected override forceZip = true;
256231
}
257232

258-
export class ApiGatewayRestApiResource extends ResourceWithS3UrlDict {
233+
class ApiGatewayRestApiResource extends ResourceWithS3UrlDict {
259234
public override resourceType = 'AWS::ApiGateway::RestApi';
260235
public override propertyName = 'BodyS3Location';
261236
protected override packageNullProperty = false;
@@ -264,7 +239,7 @@ export class ApiGatewayRestApiResource extends ResourceWithS3UrlDict {
264239
protected override versionProperty = 'Version';
265240
}
266241

267-
export class CloudFormationStackResource extends Resource {
242+
class CloudFormationStackResource extends Resource {
268243
public override resourceType = 'AWS::CloudFormation::Stack';
269244
public override propertyName = 'TemplateURL';
270245

@@ -278,7 +253,11 @@ export class CloudFormationStackResource extends Resource {
278253
throw new Error(`Invalid template path: ${templateAbsPath}`);
279254
}
280255

281-
const template = new ArtifactExporter(this.s3Service, undefined, templateAbsPath);
256+
const templateUri = pathToFileURL(templateAbsPath).href;
257+
const content = readFileSync(templateAbsPath, 'utf8');
258+
const templateType = detectDocumentType(templateUri, content).type;
259+
260+
const template = new ArtifactExporter(this.s3Service, templateType, templateUri, content);
282261
const exportedTemplateDict = await template.export(bucketName, s3KeyPrefix);
283262
const exportedTemplateStr = dump(exportedTemplateDict);
284263

src/autocomplete/CompletionFormatter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ export class CompletionFormatter {
297297
if (
298298
item?.kind === CompletionItemKind.EnumMember ||
299299
item?.kind === CompletionItemKind.Reference ||
300+
item?.kind === CompletionItemKind.Constant ||
300301
item?.kind === CompletionItemKind.Event
301302
) {
302303
return label;

src/autocomplete/CompletionRouter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export function createCompletionProviders(
311311
core.syntaxTreeManager,
312312
external.schemaRetriever,
313313
core.documentManager,
314+
external.featureFlags.get('Constants'),
314315
),
315316
);
316317
completionProviderMap.set('ParameterTypeValue', new ParameterTypeValueCompletionProvider());

src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { pseudoParameterDocsMap } from '../artifacts/PseudoParameterDocs';
33
import { Context } from '../context/Context';
44
import { IntrinsicFunction, PseudoParameter, PseudoParametersSet, TopLevelSection } from '../context/ContextType';
55
import { getEntityMap } from '../context/SectionContextBuilder';
6-
import { Mapping, Parameter, Resource } from '../context/semantic/Entity';
6+
import { Constant, Mapping, Parameter, Resource } from '../context/semantic/Entity';
77
import { EntityType } from '../context/semantic/SemanticTypes';
88
import { SyntaxTree } from '../context/syntaxtree/SyntaxTree';
99
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
1010
import { DocumentManager } from '../document/DocumentManager';
11+
import { FeatureFlag } from '../featureFlag/FeatureFlagI';
1112
import { SchemaRetriever } from '../schema/SchemaRetriever';
1213
import { LoggerFactory } from '../telemetry/LoggerFactory';
1314
import { Measure } from '../telemetry/TelemetryDecorator';
@@ -54,6 +55,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
5455
private readonly syntaxTreeManager: SyntaxTreeManager,
5556
private readonly schemaRetriever: SchemaRetriever,
5657
private readonly documentManager: DocumentManager,
58+
private readonly constantsFeatureFlag: FeatureFlag,
5759
) {}
5860

5961
@Measure({ name: 'getCompletions' })
@@ -104,14 +106,19 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
104106
syntaxTree,
105107
);
106108

107-
if (!parametersAndResourcesCompletions || parametersAndResourcesCompletions.length === 0) {
109+
const constantsCompletions = this.getConstantsCompletions(syntaxTree);
110+
111+
const allCompletions = [
112+
...this.pseudoParameterCompletionItems,
113+
...(parametersAndResourcesCompletions ?? []),
114+
...constantsCompletions,
115+
];
116+
117+
if (allCompletions.length === this.pseudoParameterCompletionItems.length) {
108118
return this.applyFuzzySearch(this.pseudoParameterCompletionItems, context.text);
109119
}
110120

111-
return this.applyFuzzySearch(
112-
[...this.pseudoParameterCompletionItems, ...parametersAndResourcesCompletions],
113-
context.text,
114-
);
121+
return this.applyFuzzySearch(allCompletions, context.text);
115122
}
116123

117124
private handleSubArguments(
@@ -125,6 +132,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
125132
syntaxTree,
126133
);
127134
const getAttCompletions = this.getGetAttCompletions(syntaxTree, context.logicalId);
135+
const constantsCompletions = this.getConstantsCompletions(syntaxTree, true);
128136

129137
const baseItems = [...this.pseudoParameterCompletionItems];
130138
if (parametersAndResourcesCompletions && parametersAndResourcesCompletions.length > 0) {
@@ -133,6 +141,9 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
133141
if (getAttCompletions.length > 0) {
134142
baseItems.push(...getAttCompletions);
135143
}
144+
if (constantsCompletions.length > 0) {
145+
baseItems.push(...constantsCompletions);
146+
}
136147

137148
// Handle ${} parameter substitution context detection
138149
const subText = this.getTextForSub(params.textDocument.uri, params.position, context);
@@ -246,6 +257,49 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
246257
return completionItems;
247258
}
248259

260+
private getConstantsAsCompletionItems(
261+
constantsMap: ReadonlyMap<string, Context>,
262+
stringOnly: boolean = false,
263+
): CompletionItem[] {
264+
const completionItems: CompletionItem[] = [];
265+
for (const [constantName, context] of constantsMap) {
266+
const constant = context.entity as Constant;
267+
268+
if (stringOnly && typeof constant.value !== 'string') {
269+
continue;
270+
}
271+
272+
const valuePreview =
273+
typeof constant.value === 'string'
274+
? constant.value
275+
: typeof constant.value === 'object'
276+
? '[Object]'
277+
: String(constant.value);
278+
279+
completionItems.push(
280+
createCompletionItem(constantName, CompletionItemKind.Constant, {
281+
detail: `Constant`,
282+
documentation: `Value: ${valuePreview}`,
283+
}),
284+
);
285+
}
286+
287+
return completionItems;
288+
}
289+
290+
private getConstantsCompletions(syntaxTree: SyntaxTree, stringOnly: boolean = false): CompletionItem[] {
291+
if (!this.constantsFeatureFlag.isEnabled()) {
292+
return [];
293+
}
294+
295+
const constantsMap = getEntityMap(syntaxTree, TopLevelSection.Constants);
296+
if (!constantsMap || constantsMap.size === 0) {
297+
return [];
298+
}
299+
300+
return this.getConstantsAsCompletionItems(constantsMap, stringOnly);
301+
}
302+
249303
private shouldIncludeResourceCompletions(context: Context): boolean {
250304
// Only provide resource completions in Resources and Outputs sections
251305
return context.section === TopLevelSection.Resources || context.section === TopLevelSection.Outputs;

src/codeLens/StackActionsCodeLens.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
55
import { Document } from '../document/Document';
66

77
const STACK_ACTION_TITLES = {
8-
VALIDATE_DEPLOYMENT: 'Validate Deployment',
9-
DEPLOY: 'Deploy Template',
8+
DEPLOY: 'Validate and Deploy',
109
} as const;
1110

1211
const STACK_ACTION_COMMANDS = {
13-
VALIDATE_DEPLOYMENT: 'aws.cloudformation.api.validateDeployment',
1412
DEPLOY: 'aws.cloudformation.api.deployTemplate',
1513
} as const;
1614

@@ -49,14 +47,6 @@ export function getStackActionsCodeLenses(
4947
const range = Range.create(Position.create(codeLensLine, 0), Position.create(codeLensLine, 0));
5048

5149
return [
52-
{
53-
range,
54-
command: {
55-
title: STACK_ACTION_TITLES.VALIDATE_DEPLOYMENT,
56-
command: STACK_ACTION_COMMANDS.VALIDATE_DEPLOYMENT,
57-
arguments: [uri],
58-
},
59-
},
6050
{
6151
range,
6252
command: {

src/context/semantic/Intrinsics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export function normalizeIntrinsicFunction(text: string): string {
1111
}
1212
return text;
1313
}
14+
15+
export function normalizeIntrinsicFunctionAndCondition(text: string): string {
16+
return text === '!Condition' ? 'Condition' : normalizeIntrinsicFunction(text);
17+
}

0 commit comments

Comments
 (0)