Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 3 additions & 58 deletions src/autocomplete/IntrinsicFunctionArgumentCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DocumentManager } from '../document/DocumentManager';
import { SchemaRetriever } from '../schema/SchemaRetriever';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { getFuzzySearchFunction } from '../utils/FuzzySearchUtil';
import { determineGetAttPosition, extractGetAttResourceLogicalId } from '../utils/GetAttUtils';
import { CompletionProvider } from './CompletionProvider';
import { createCompletionItem, createMarkupContent, createReplacementRange } from './CompletionUtils';

Expand Down Expand Up @@ -231,7 +232,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
return undefined;
}

const position = this.determineGetAttPosition(intrinsicFunction.args, context);
const position = determineGetAttPosition(intrinsicFunction.args, context);

if (position === 1) {
return this.getGetAttResourceCompletions(resourceEntities, intrinsicFunction.args, context);
Expand Down Expand Up @@ -636,45 +637,6 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
}
}

private determineGetAttPosition(args: unknown, context: Context): number {
if (typeof args === 'string') {
const dotIndex = args.indexOf('.');
if (dotIndex === -1) {
return 1;
}

const resourcePart = args.slice(0, dotIndex);

if (context.text === resourcePart) {
return 1;
}

if (context.text.length > 0 && resourcePart.startsWith(context.text)) {
return 1;
}

return 2;
}

if (!Array.isArray(args)) {
return 0;
}

if (args.length === 0) {
return 1;
}

if (args.length === 1 && args[0] === context.text) {
return 1;
}

if (args.length >= 2 && args[1] === context.text) {
return 2;
}

return 2;
}

private getGetAttResourceCompletions(
resourceEntities: Map<string, Context>,
args: unknown,
Expand Down Expand Up @@ -712,7 +674,7 @@ export class IntrinsicFunctionArgumentCompletionProvider implements CompletionPr
args: unknown,
context: Context,
): CompletionItem[] | undefined {
const resourceLogicalId = this.extractGetAttResourceLogicalId(args);
const resourceLogicalId = extractGetAttResourceLogicalId(args);

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

private extractGetAttResourceLogicalId(args: unknown): string | undefined {
if (typeof args === 'string') {
const dotIndex = args.indexOf('.');
if (dotIndex !== -1) {
return args.slice(0, Math.max(0, dotIndex));
}
return args;
}

if (Array.isArray(args) && args.length > 0 && typeof args[0] === 'string') {
// Array format
return args[0];
}

return undefined;
}

private isRefObject(value: unknown): value is { Ref: unknown } | { '!Ref': unknown } {
return typeof value === 'object' && value !== null && ('Ref' in value || '!Ref' in value);
}
Expand Down
2 changes: 2 additions & 0 deletions src/context/semantic/LogicalIdReferenceFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
}
if (text.includes('!GetAtt')) {
extractMatches(text, YamlGetAtt, logicalIds);
extractMatches(text, YamlGetAttArray, logicalIds);
}
if (text.includes('!FindInMap')) {
extractMatches(text, YamlFindInMap, logicalIds);
Expand Down Expand Up @@ -218,6 +219,7 @@

const YamlRef = /!Ref\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !Ref LogicalId - YAML short form reference
const YamlGetAtt = /!GetAtt\s+([A-Za-z][A-Za-z0-9]*)/g; // Matches !GetAtt LogicalId.Attribute - YAML short form get attribute
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
const YamlFindInMap = /!FindInMap\s+\[\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches !FindInMap [MappingName, Key1, Key2] - YAML short form mapping lookup
const YamlSub = /!Sub\s+["']?([^"'\n]+)["']?/g; // Matches !Sub "template string" - YAML short form string substitution
const YamlRefColon = /Ref:\s*([A-Za-z][A-Za-z0-9]*)/g; // Matches Ref: LogicalId - YAML long form reference
Expand All @@ -234,7 +236,7 @@
const ValidLogicalId = /^[A-Za-z][A-Za-z0-9.]+$/;

// Validated these regex, they will fail fast with ?= lookahead
const YamlListDep = /DependsOn:\s*\n(\s*-\s*[A-Za-z][A-Za-z0-9]*(?:\s+-\s*[A-Za-z][A-Za-z0-9]*)*)/g; // Matches DependsOn: followed by YAML list items

Check warning on line 239 in src/context/semantic/LogicalIdReferenceFinder.ts

View workflow job for this annotation

GitHub Actions / pr-build-test-nodejs

Unsafe Regular Expression

const YamlInlineListItem = /^(?=\s*-)\s*-\s+([A-Za-z][A-Za-z0-9]*)/gm; // Matches - LogicalId - standalone list items (for DependsOn arrays)

Expand Down
3 changes: 2 additions & 1 deletion src/hover/HoverProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Position } from 'vscode-languageserver-protocol';
import { Context } from '../context/Context';

export interface HoverProvider {
getInformation(context: Context): string | undefined;
getInformation(context: Context, position?: Position): string | undefined;
}
9 changes: 7 additions & 2 deletions src/hover/HoverRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export class HoverRouter implements SettingsConfigurable, Closeable {

// Check for intrinsic function arguments first
if (context.intrinsicContext.inIntrinsic()) {
const doc = this.hoverProviderMap.get(HoverType.IntrinsicFunctionArgument)?.getInformation(context);
const doc = this.hoverProviderMap
.get(HoverType.IntrinsicFunctionArgument)
?.getInformation(context, textDocPosParams.position);
if (doc) {
return doc;
}
Expand Down Expand Up @@ -126,7 +128,10 @@ export class HoverRouter implements SettingsConfigurable, Closeable {
hoverProviderMap.set(HoverType.Condition, new ConditionHoverProvider());
hoverProviderMap.set(HoverType.Mapping, new MappingHoverProvider());
hoverProviderMap.set(HoverType.IntrinsicFunction, new IntrinsicFunctionHoverProvider());
hoverProviderMap.set(HoverType.IntrinsicFunctionArgument, new IntrinsicFunctionArgumentHoverProvider());
hoverProviderMap.set(
HoverType.IntrinsicFunctionArgument,
new IntrinsicFunctionArgumentHoverProvider(schemaRetriever),
);
return hoverProviderMap;
}

Expand Down
119 changes: 108 additions & 11 deletions src/hover/IntrinsicFunctionArgumentHoverProvider.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Position } from 'vscode-languageserver-protocol';
import { Context } from '../context/Context';
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet } from '../context/ContextType';
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet, TopLevelSection } from '../context/ContextType';
import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities';
import { Resource } from '../context/semantic/Entity';
import { EntityType } from '../context/semantic/SemanticTypes';
import { SchemaRetriever } from '../schema/SchemaRetriever';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { determineGetAttPosition, extractAttributeName, extractGetAttResourceLogicalId } from '../utils/GetAttUtils';
import { formatIntrinsicArgumentHover, getResourceAttributeValueDoc } from './HoverFormatter';
import { HoverProvider } from './HoverProvider';

const log = LoggerFactory.getLogger('IntrinsicFunctionArgumentHoverProvider');

export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
constructor() {}
constructor(private readonly schemaRetriever: SchemaRetriever) {}

getInformation(context: Context): string | undefined {
getInformation(context: Context, position?: Position): string | undefined {
// Only handle contexts that are inside intrinsic functions
if (!context.intrinsicContext.inIntrinsic()) {
if (!context.intrinsicContext.inIntrinsic() || context.isIntrinsicFunc) {
return undefined;
}

Expand All @@ -28,7 +36,7 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
return this.handleRefArgument(context);
}
case IntrinsicFunction.GetAtt: {
return this.handleGetAttArgument(context);
return this.handleGetAttArgument(context, position);
}
// Add other intrinsic function types as needed
default: {
Expand All @@ -43,9 +51,13 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
return undefined;
}

// Extract logical ID (handle dot notation like "MyBucket.Arn")
const dotIndex = context.text.indexOf('.');
const logicalId = dotIndex === -1 ? context.text : context.text.slice(0, dotIndex);

// Look for the referenced entity in related entities
for (const [, section] of context.relatedEntities.entries()) {
const relatedContext = section.get(context.text);
const relatedContext = section.get(logicalId);
if (relatedContext) {
return this.buildSchemaAndFormat(relatedContext);
}
Expand All @@ -54,11 +66,27 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
return undefined;
}

private handleGetAttArgument(context: Context): string | undefined {
// For !GetAtt, we might want to handle resource.attribute references
// This could be implemented similarly to handleRefArgument but with
// additional logic for attribute-specific information
return this.handleRefArgument(context); // For now, use same logic as Ref
private handleGetAttArgument(context: Context, position?: Position): string | undefined {
if (!(context instanceof ContextWithRelatedEntities)) {
return undefined;
}

const intrinsicFunction = context.intrinsicContext.intrinsicFunction();
if (!intrinsicFunction) {
return undefined;
}

const getAttPosition = determineGetAttPosition(intrinsicFunction.args, context, position);

if (getAttPosition === 1) {
// Hovering over resource name
return this.handleRefArgument(context);
} else if (getAttPosition === 2) {
// Hovering over attribute name
return this.getGetAttAttributeHover(context, intrinsicFunction.args);
}

return undefined;
}

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

return undefined;
}

/**
* Gets hover information for GetAtt attribute names
*/
private getGetAttAttributeHover(context: ContextWithRelatedEntities, args: unknown): string | undefined {
const resourceLogicalId = extractGetAttResourceLogicalId(args);
if (!resourceLogicalId) {
return undefined;
}

const resourcesSection = context.relatedEntities.get(TopLevelSection.Resources);
if (!resourcesSection) {
return undefined;
}

const resourceContext = resourcesSection.get(resourceLogicalId);
if (!resourceContext?.entity || resourceContext.entity.entityType !== EntityType.Resource) {
return undefined;
}

const resource = resourceContext.entity as Resource;
const resourceType = resource.Type;
if (!resourceType || typeof resourceType !== 'string') {
return undefined;
}

const attributeName = extractAttributeName(args, context);
if (!attributeName) {
return undefined;
}

return this.getAttributeDocumentation(resourceType, attributeName);
}

/**
* Gets documentation for a specific resource attribute from the schema
*/
private getAttributeDocumentation(resourceType: string, attributeName: string): string | undefined {
const schema = this.schemaRetriever.getDefault().schemas.get(resourceType);

// Provide fallback description even when schema is not available
let description = `**${attributeName}** attribute of **${resourceType}**\n\nReturns the value of this attribute when used with the GetAtt intrinsic function.`;

if (schema) {
const jsonPointerPath = `/properties/${attributeName.replaceAll('.', '/')}`;

try {
const resolvedSchemas = schema.resolveJsonPointerPath(jsonPointerPath);
if (resolvedSchemas.length === 1) {
const firstSchema = resolvedSchemas[0];
if (firstSchema.description) {
description = firstSchema.description;
}
}
} catch (error) {
log.debug({ error, resourceType, attributeName }, 'Error resolving attribute documentation');
}
}

return this.formatAttributeHover(resourceType, description);
}

/**
* Formats the hover information for GetAtt attributes
*/
private formatAttributeHover(resourceType: string, description: string): string {
const lines = [`**GetAtt attribute for ${resourceType}**`, '', description];
return lines.join('\n');
}
}
Loading
Loading