Skip to content

Commit 98b1485

Browse files
authored
Fix/Hover on GetAtt attributes shows proper information for logicalName and Attribute (#118)
1 parent aa6d00b commit 98b1485

File tree

8 files changed

+645
-86
lines changed

8 files changed

+645
-86
lines changed

src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DocumentManager } from '../document/DocumentManager';
1111
import { SchemaRetriever } from '../schema/SchemaRetriever';
1212
import { LoggerFactory } from '../telemetry/LoggerFactory';
1313
import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil';
14+
import { determineGetAttPosition, extractGetAttResourceLogicalId } from '../utils/GetAttUtils';
1415
import { CompletionProvider } from './CompletionProvider';
1516
import { createCompletionItem, createMarkupContent, createReplacementRange } from './CompletionUtils';
1617

@@ -231,7 +232,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
231232
return undefined;
232233
}
233234

234-
const position = this.determineGetAttPosition(intrinsicFunction.args, context);
235+
const position = determineGetAttPosition(intrinsicFunction.args, context);
235236

236237
if (position === 1) {
237238
return this.getGetAttResourceCompletions(resourceEntities, intrinsicFunction.args, context);
@@ -636,45 +637,6 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
636637
}
637638
}
638639

639-
private determineGetAttPosition(args: unknown, context: Context): number {
640-
if (typeof args === 'string') {
641-
const dotIndex = args.indexOf('.');
642-
if (dotIndex === -1) {
643-
return 1;
644-
}
645-
646-
const resourcePart = args.slice(0, dotIndex);
647-
648-
if (context.text === resourcePart) {
649-
return 1;
650-
}
651-
652-
if (context.text.length > 0 && resourcePart.startsWith(context.text)) {
653-
return 1;
654-
}
655-
656-
return 2;
657-
}
658-
659-
if (!Array.isArray(args)) {
660-
return 0;
661-
}
662-
663-
if (args.length === 0) {
664-
return 1;
665-
}
666-
667-
if (args.length === 1 && args[0] === context.text) {
668-
return 1;
669-
}
670-
671-
if (args.length >= 2 && args[1] === context.text) {
672-
return 2;
673-
}
674-
675-
return 2;
676-
}
677-
678640
private getGetAttResourceCompletions(
679641
resourceEntities: Map<string, Context>,
680642
args: unknown,
@@ -712,7 +674,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
712674
args: unknown,
713675
context: Context,
714676
): CompletionItem[] | undefined {
715-
const resourceLogicalId = this.extractGetAttResourceLogicalId(args);
677+
const resourceLogicalId = extractGetAttResourceLogicalId(args);
716678

717679
if (!resourceLogicalId) {
718680
return undefined;
@@ -786,23 +748,6 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
786748
return context.text.length > 0 ? this.attributeFuzzySearch(completionItems, context.text) : completionItems;
787749
}
788750

789-
private extractGetAttResourceLogicalId(args: unknown): string | undefined {
790-
if (typeof args === 'string') {
791-
const dotIndex = args.indexOf('.');
792-
if (dotIndex !== -1) {
793-
return args.slice(0, Math.max(0, dotIndex));
794-
}
795-
return args;
796-
}
797-
798-
if (Array.isArray(args) && args.length > 0 && typeof args[0] === 'string') {
799-
// Array format
800-
return args[0];
801-
}
802-
803-
return undefined;
804-
}
805-
806751
private isRefObject(value: unknown): value is { Ref: unknown } | { '!Ref': unknown } {
807752
return typeof value === 'object' && value !== null && ('Ref' in value || '!Ref' in value);
808753
}

src/context/semantic/LogicalIdReferenceFinder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function findYamlIntrinsicReferences(text: string, logicalIds: Set<string>): voi
8181
}
8282
if (text.includes('!GetAtt')) {
8383
extractMatches(text, YamlGetAtt, logicalIds);
84+
extractMatches(text, YamlGetAttArray, logicalIds);
8485
}
8586
if (text.includes('!FindInMap')) {
8687
extractMatches(text, YamlFindInMap, logicalIds);
@@ -218,6 +219,7 @@ const JsonArrayItem = /"([A-Za-z][A-Za-z0-9]*)"/g; // Matches "LogicalId" within
218219

219220
const YamlRef = /!Ref\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Ref LogicalId - YAML short form reference
220221
const YamlGetAtt = /!GetAtt\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute
222+
const YamlGetAttArray = /!GetAtt\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt [LogicalId, Attribute] - YAML short form get attribute with array syntax
221223
const YamlFindInMap = /!FindInMap\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !FindInMap [MappingName, Key1, Key2] - YAML short form mapping lookup
222224
const YamlSub = /!Sub\s+["']?([^"'\n]+)["']?/g; // Matches !Sub "template string" - YAML short form string substitution
223225
const YamlRefColon = /Ref:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Ref: LogicalId - YAML long form reference

src/hover/HoverProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { Position } from 'vscode-languageserver-protocol';
12
import { Context } from '../context/Context';
23

34
export interface HoverProvider {
4-
getInformation(context: Context): string | undefined;
5+
getInformation(context: Context, position?: Position): string | undefined;
56
}

src/hover/HoverRouter.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
7777

7878
// Check for intrinsic function arguments first
7979
if (context.intrinsicContext.inIntrinsic()) {
80-
const doc = this.hoverProviderMap.get(HoverType.IntrinsicFunctionArgument)?.getInformation(context);
80+
const doc = this.hoverProviderMap
81+
.get(HoverType.IntrinsicFunctionArgument)
82+
?.getInformation(context, textDocPosParams.position);
8183
if (doc) {
8284
return doc;
8385
}
@@ -126,7 +128,10 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
126128
hoverProviderMap.set(HoverType.Condition, new ConditionHoverProvider());
127129
hoverProviderMap.set(HoverType.Mapping, new MappingHoverProvider());
128130
hoverProviderMap.set(HoverType.IntrinsicFunction, new IntrinsicFunctionHoverProvider());
129-
hoverProviderMap.set(HoverType.IntrinsicFunctionArgument, new IntrinsicFunctionArgumentHoverProvider());
131+
hoverProviderMap.set(
132+
HoverType.IntrinsicFunctionArgument,
133+
new IntrinsicFunctionArgumentHoverProvider(schemaRetriever),
134+
);
130135
return hoverProviderMap;
131136
}
132137

src/hover/IntrinsicFunctionArgumentHoverProvider.ts

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
import { Position } from 'vscode-languageserver-protocol';
12
import { Context } from '../context/Context';
2-
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet } from '../context/ContextType';
3+
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet, TopLevelSection } from '../context/ContextType';
34
import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities';
5+
import { Resource } from '../context/semantic/Entity';
6+
import { EntityType } from '../context/semantic/SemanticTypes';
7+
import { SchemaRetriever } from '../schema/SchemaRetriever';
8+
import { LoggerFactory } from '../telemetry/LoggerFactory';
9+
import { determineGetAttPosition, extractAttributeName, extractGetAttResourceLogicalId } from '../utils/GetAttUtils';
410
import { formatIntrinsicArgumentHover, getResourceAttributeValueDoc } from './HoverFormatter';
511
import { HoverProvider } from './HoverProvider';
612

13+
const log = LoggerFactory.getLogger('IntrinsicFunctionArgumentHoverProvider');
14+
715
export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
8-
constructor() {}
16+
constructor(private readonly schemaRetriever: SchemaRetriever) {}
917

10-
getInformation(context: Context): string | undefined {
18+
getInformation(context: Context, position?: Position): string | undefined {
1119
// Only handle contexts that are inside intrinsic functions
12-
if (!context.intrinsicContext.inIntrinsic()) {
20+
if (!context.intrinsicContext.inIntrinsic() || context.isIntrinsicFunc) {
1321
return undefined;
1422
}
1523

@@ -28,7 +36,7 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
2836
return this.handleRefArgument(context);
2937
}
3038
case IntrinsicFunction.GetAtt: {
31-
return this.handleGetAttArgument(context);
39+
return this.handleGetAttArgument(context, position);
3240
}
3341
// Add other intrinsic function types as needed
3442
default: {
@@ -43,9 +51,13 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
4351
return undefined;
4452
}
4553

54+
// Extract logical ID (handle dot notation like "MyBucket.Arn")
55+
const dotIndex = context.text.indexOf('.');
56+
const logicalId = dotIndex === -1 ? context.text : context.text.slice(0, dotIndex);
57+
4658
// Look for the referenced entity in related entities
4759
for (const [, section] of context.relatedEntities.entries()) {
48-
const relatedContext = section.get(context.text);
60+
const relatedContext = section.get(logicalId);
4961
if (relatedContext) {
5062
return this.buildSchemaAndFormat(relatedContext);
5163
}
@@ -54,11 +66,27 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
5466
return undefined;
5567
}
5668

57-
private handleGetAttArgument(context: Context): string | undefined {
58-
// For !GetAtt, we might want to handle resource.attribute references
59-
// This could be implemented similarly to handleRefArgument but with
60-
// additional logic for attribute-specific information
61-
return this.handleRefArgument(context); // For now, use same logic as Ref
69+
private handleGetAttArgument(context: Context, position?: Position): string | undefined {
70+
if (!(context instanceof ContextWithRelatedEntities)) {
71+
return undefined;
72+
}
73+
74+
const intrinsicFunction = context.intrinsicContext.intrinsicFunction();
75+
if (!intrinsicFunction) {
76+
return undefined;
77+
}
78+
79+
const getAttPosition = determineGetAttPosition(intrinsicFunction.args, context, position);
80+
81+
if (getAttPosition === 1) {
82+
// Hovering over resource name
83+
return this.handleRefArgument(context);
84+
} else if (getAttPosition === 2) {
85+
// Hovering over attribute name
86+
return this.getGetAttAttributeHover(context, intrinsicFunction.args);
87+
}
88+
89+
return undefined;
6290
}
6391

6492
private buildSchemaAndFormat(relatedContext: Context): string | undefined {
@@ -80,4 +108,73 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
80108

81109
return undefined;
82110
}
111+
112+
/**
113+
* Gets hover information for GetAtt attribute names
114+
*/
115+
private getGetAttAttributeHover(context: ContextWithRelatedEntities, args: unknown): string | undefined {
116+
const resourceLogicalId = extractGetAttResourceLogicalId(args);
117+
if (!resourceLogicalId) {
118+
return undefined;
119+
}
120+
121+
const resourcesSection = context.relatedEntities.get(TopLevelSection.Resources);
122+
if (!resourcesSection) {
123+
return undefined;
124+
}
125+
126+
const resourceContext = resourcesSection.get(resourceLogicalId);
127+
if (!resourceContext?.entity || resourceContext.entity.entityType !== EntityType.Resource) {
128+
return undefined;
129+
}
130+
131+
const resource = resourceContext.entity as Resource;
132+
const resourceType = resource.Type;
133+
if (!resourceType || typeof resourceType !== 'string') {
134+
return undefined;
135+
}
136+
137+
const attributeName = extractAttributeName(args, context);
138+
if (!attributeName) {
139+
return undefined;
140+
}
141+
142+
return this.getAttributeDocumentation(resourceType, attributeName);
143+
}
144+
145+
/**
146+
* Gets documentation for a specific resource attribute from the schema
147+
*/
148+
private getAttributeDocumentation(resourceType: string, attributeName: string): string | undefined {
149+
const schema = this.schemaRetriever.getDefault().schemas.get(resourceType);
150+
151+
// Provide fallback description even when schema is not available
152+
let description = `**${attributeName}** attribute of **${resourceType}**\n\nReturns the value of this attribute when used with the GetAtt intrinsic function.`;
153+
154+
if (schema) {
155+
const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/')}`;
156+
157+
try {
158+
const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath);
159+
if (resolvedSchemas.length === 1) {
160+
const firstSchema = resolvedSchemas[0];
161+
if (firstSchema.description) {
162+
description = firstSchema.description;
163+
}
164+
}
165+
} catch (error) {
166+
log.debug({ error, resourceType, attributeName }, 'Error resolving attribute documentation');
167+
}
168+
}
169+
170+
return this.formatAttributeHover(resourceType, description);
171+
}
172+
173+
/**
174+
* Formats the hover information for GetAtt attributes
175+
*/
176+
private formatAttributeHover(resourceType: string, description: string): string {
177+
const lines = [`**GetAtt attribute for ${resourceType}**`, '', description];
178+
return lines.join('\n');
179+
}
83180
}

0 commit comments

Comments
 (0)