Skip to content

Commit 6ea12aa

Browse files
authored
Hover support for Constants (#257)
1 parent f324e3a commit 6ea12aa

16 files changed

+302
-13
lines changed

src/artifacts/TemplateSectionDocs.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,21 @@ function getTemplateSectionDocsMap(): Map<TopLevelSection, string> {
206206
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-description-structure.html)',
207207
].join('\n'),
208208
);
209+
// TODO: Update with official AWS documentation when available
210+
templateSectionDocsMap.set(
211+
TopLevelSection.Constants,
212+
[
213+
'**Constants**',
214+
'\n',
215+
'---',
216+
'The optional `Constants` section allows you to define reusable values that can be referenced throughout your template. Constants can be strings or objects and are referenced directly by their name in intrinsic functions like `Ref` and `Sub`.',
217+
'\n',
218+
'Constants are useful for defining values that are used multiple times in your template, making your templates more maintainable and reducing duplication.',
219+
'\n',
220+
'**Note:** This feature requires the `AWS::LanguageExtensions` transform.',
221+
'\n',
222+
'[Source Documentation](https://docs.aws.amazon.com/)',
223+
].join('\n'),
224+
);
209225
return templateSectionDocsMap;
210226
}

src/context/ContextWithRelatedEntities.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class ContextWithRelatedEntities extends Context {
7272
TopLevelSection.Mappings,
7373
TopLevelSection.Conditions,
7474
TopLevelSection.Resources,
75+
TopLevelSection.Constants,
7576
]);
7677

7778
const logicalIds = referencedLogicalIds(

src/hover/ConstantHoverProvider.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Context } from '../context/Context';
2+
import { Constant } from '../context/semantic/Entity';
3+
import { Measure } from '../telemetry/TelemetryDecorator';
4+
import { formatConstantHover } from './HoverFormatter';
5+
import { HoverProvider } from './HoverProvider';
6+
7+
export class ConstantHoverProvider implements HoverProvider {
8+
@Measure({ name: 'getInformation' })
9+
getInformation(context: Context): string | undefined {
10+
const constant = context.entity as Constant;
11+
if (!constant?.name) {
12+
return undefined;
13+
}
14+
15+
return formatConstantHover(constant);
16+
}
17+
}

src/hover/HoverFormatter.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/Dele
22
import { updateReplacePolicyValueDocsMap } from '../artifacts/resourceAttributes/UpdateReplacePolicyPropertyDocs-1';
33
import { Context } from '../context/Context';
44
import { ResourceAttribute } from '../context/ContextType';
5-
import { Condition, Entity, Mapping, Parameter, Resource } from '../context/semantic/Entity';
5+
import { Condition, Constant, Entity, Mapping, Parameter, Resource } from '../context/semantic/Entity';
66
import { EntityType } from '../context/semantic/SemanticTypes';
77
import { PropertyType } from '../schema/ResourceSchema';
88

@@ -887,6 +887,11 @@ export function formatIntrinsicArgumentHover(context: Context): string {
887887
doc.push(`**Mapping:** ${mapping.name}`);
888888
break;
889889
}
890+
891+
case EntityType.Constant: {
892+
doc.push(formatConstantHover(context.entity as Constant));
893+
break;
894+
}
890895
}
891896

892897
return doc.filter((item) => item.trim() !== '').join('\n\n');
@@ -1105,6 +1110,24 @@ export function formatParameterHover(parameter: Parameter): string {
11051110
return doc.filter((item) => item.trim() !== '').join('\n\n');
11061111
}
11071112

1113+
/**
1114+
* Formats hover information for a constant entity
1115+
*/
1116+
export function formatConstantHover(constant: Constant): string {
1117+
const doc: string[] = [];
1118+
1119+
const valueType = typeof constant.value === 'string' ? 'string' : 'object';
1120+
doc.push(`\`\`\`typescript\n(constant) ${constant.name}: ${valueType}\n\`\`\``, '---');
1121+
1122+
if (typeof constant.value === 'string') {
1123+
doc.push(`**Value:** ${constant.value}`);
1124+
} else if (typeof constant.value === 'object') {
1125+
doc.push(`**Value:** [Object]`);
1126+
}
1127+
1128+
return doc.filter((item) => item.trim() !== '').join('\n\n');
1129+
}
1130+
11081131
/**
11091132
* Formats hover information for a resource entity
11101133
*/

src/hover/HoverRouter.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { ContextManager } from '../context/ContextManager';
44
import { TopLevelSection, IntrinsicFunction } from '../context/ContextType';
55
import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities';
66
import { EntityType } from '../context/semantic/SemanticTypes';
7+
import { FeatureFlag } from '../featureFlag/FeatureFlagI';
78
import { SchemaRetriever } from '../schema/SchemaRetriever';
89
import { ISettingsSubscriber, SettingsConfigurable, SettingsSubscription } from '../settings/ISettingsSubscriber';
910
import { DefaultSettings, HoverSettings } from '../settings/Settings';
1011
import { LoggerFactory } from '../telemetry/LoggerFactory';
1112
import { Track } from '../telemetry/TelemetryDecorator';
1213
import { Closeable } from '../utils/Closeable';
1314
import { ConditionHoverProvider } from './ConditionHoverProvider';
15+
import { ConstantHoverProvider } from './ConstantHoverProvider';
1416
import { HoverProvider } from './HoverProvider';
1517
import { IntrinsicFunctionArgumentHoverProvider } from './IntrinsicFunctionArgumentHoverProvider';
1618
import { IntrinsicFunctionHoverProvider } from './IntrinsicFunctionHoverProvider';
@@ -31,6 +33,7 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
3133
constructor(
3234
private readonly contextManager: ContextManager,
3335
schemaRetriever: SchemaRetriever,
36+
private readonly constantsFeatureFlag: FeatureFlag,
3437
) {
3538
this.hoverProviderMap = this.createHoverProviders(schemaRetriever);
3639
}
@@ -102,6 +105,11 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
102105
if (doc) {
103106
return doc;
104107
}
108+
} else if (context.section === TopLevelSection.Constants && this.constantsFeatureFlag.isEnabled()) {
109+
const doc = this.hoverProviderMap.get(HoverType.Constant)?.getInformation(context);
110+
if (doc) {
111+
return doc;
112+
}
105113
} else if (context.section === TopLevelSection.Outputs && this.isOutputAttribute(context)) {
106114
const doc = this.hoverProviderMap.get(HoverType.OutputSectionField)?.getInformation(context);
107115
if (doc) {
@@ -128,14 +136,15 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
128136

129137
private createHoverProviders(schemaRetriever: SchemaRetriever): Map<HoverType, HoverProvider> {
130138
const hoverProviderMap = new Map<HoverType, HoverProvider>();
131-
hoverProviderMap.set(HoverType.TopLevelSection, new TemplateSectionHoverProvider());
139+
hoverProviderMap.set(HoverType.TopLevelSection, new TemplateSectionHoverProvider(this.constantsFeatureFlag));
132140
hoverProviderMap.set(HoverType.ResourceSection, new ResourceSectionHoverProvider(schemaRetriever));
133141
hoverProviderMap.set(HoverType.Parameter, new ParameterHoverProvider());
134142
hoverProviderMap.set(HoverType.ParameterAttribute, new ParameterAttributeHoverProvider());
135143
hoverProviderMap.set(HoverType.OutputSectionField, new OutputSectionFieldHoverProvider());
136144
hoverProviderMap.set(HoverType.PseudoParameter, new PseudoParameterHoverProvider());
137145
hoverProviderMap.set(HoverType.Condition, new ConditionHoverProvider());
138146
hoverProviderMap.set(HoverType.Mapping, new MappingHoverProvider());
147+
hoverProviderMap.set(HoverType.Constant, new ConstantHoverProvider());
139148
hoverProviderMap.set(HoverType.IntrinsicFunction, new IntrinsicFunctionHoverProvider());
140149
hoverProviderMap.set(
141150
HoverType.IntrinsicFunctionArgument,
@@ -184,6 +193,12 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
184193
case EntityType.Resource: {
185194
return this.hoverProviderMap.get(HoverType.ResourceSection)?.getInformation(context);
186195
}
196+
case EntityType.Constant: {
197+
if (this.constantsFeatureFlag.isEnabled()) {
198+
return this.hoverProviderMap.get(HoverType.Constant)?.getInformation(context);
199+
}
200+
return undefined;
201+
}
187202
}
188203

189204
return undefined;
@@ -201,4 +216,5 @@ enum HoverType {
201216
PseudoParameter,
202217
Condition,
203218
Mapping,
219+
Constant,
204220
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { templateSectionDocsMap } from '../artifacts/TemplateSectionDocs';
22
import { Context } from '../context/Context';
33
import { TopLevelSection } from '../context/ContextType';
4+
import { FeatureFlag } from '../featureFlag/FeatureFlagI';
45
import { Measure } from '../telemetry/TelemetryDecorator';
56
import { HoverProvider } from './HoverProvider';
67

78
export class TemplateSectionHoverProvider implements HoverProvider {
9+
constructor(private readonly constantsFeatureFlag: FeatureFlag) {}
10+
811
@Measure({ name: 'getInformation' })
912
getInformation(context: Context): string | undefined {
13+
if (context.text === String(TopLevelSection.Constants) && !this.constantsFeatureFlag.isEnabled()) {
14+
return undefined;
15+
}
16+
1017
return templateSectionDocsMap.get(context.text as TopLevelSection);
1118
}
1219
}

src/server/CfnLspProviders.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export class CfnLspProviders implements Configurables, Closeable {
8080
new RelatedResourcesSnippetProvider(core.documentManager, core.syntaxTreeManager, external.schemaRetriever);
8181
this.s3Service = overrides.s3Service ?? new S3Service(external.awsClient);
8282

83-
this.hoverRouter = overrides.hoverRouter ?? new HoverRouter(core.contextManager, external.schemaRetriever);
83+
this.hoverRouter =
84+
overrides.hoverRouter ??
85+
new HoverRouter(core.contextManager, external.schemaRetriever, external.featureFlags.get('Constants'));
8486
this.completionRouter = overrides.completionRouter ?? CompletionRouter.create(core, external, this);
8587

8688
this.definitionProvider = overrides.definitionProvider ?? new DefinitionProvider(core.contextManager);

tst/resources/templates/constants.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
"Transform": "AWS::LanguageExtensions",
44
"Constants": {
55
"foo": "bar",
6-
"sub": "${foo}-abc-${AWS::AccountId}",
6+
"sub": {
7+
"Fn::Sub": "${foo}-abc-${AWS::AccountId}"
8+
},
79
"obj": {
810
"TestObject": {
911
"A": "b"

tst/resources/templates/constants.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Transform: AWS::LanguageExtensions
33

44
Constants:
55
foo: bar
6-
sub: "${foo}-abc-${AWS::AccountId}"
6+
sub: !Sub ${foo}-abc-${AWS::AccountId}
77
obj:
88
TestObject:
99
A: b

tst/unit/context/Context.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,7 @@ Resources:
867867
expect(constant.value).toBe('bar');
868868
});
869869

870-
it('should create Constant entity with string interpolation', () => {
870+
it('should create Constant entity with Sub intrinsic function', () => {
871871
const context = getContextAt(5, 2, constantsYamlUri); // sub constant
872872

873873
expect(context).toBeDefined();
@@ -876,8 +876,8 @@ Resources:
876876

877877
const constant = entity as Constant;
878878
expect(constant.name).toBe('sub');
879-
expect(constant.value).toContain('${foo}');
880-
expect(constant.value).toContain('${AWS::AccountId}');
879+
expect(constant.value).toBeDefined();
880+
expect(typeof constant.value).toBe('object');
881881
});
882882

883883
it('should create Constant entity with object value', () => {
@@ -923,7 +923,7 @@ Resources:
923923
});
924924

925925
it('should parse constant with object value in JSON', () => {
926-
const context = getContextAt(6, 6, constantsJsonUri); // Position at "obj"
926+
const context = getContextAt(9, 6, constantsJsonUri); // Position at "obj"
927927

928928
expect(context).toBeDefined();
929929
const entity = context!.entity;

0 commit comments

Comments
 (0)