diff --git a/src/artifacts/OutputSectionFieldDocs.ts b/src/artifacts/OutputSectionFieldDocs.ts new file mode 100644 index 00000000..d6153fd8 --- /dev/null +++ b/src/artifacts/OutputSectionFieldDocs.ts @@ -0,0 +1,50 @@ +export const outputSectionFieldDocsMap = getOutputSectionFieldDocsMap(); + +function getOutputSectionFieldDocsMap(): Map { + const outputSectionFieldDocsMap = new Map(); + + outputSectionFieldDocsMap.set( + 'Description', + [ + '**Description (optional)**', + '\n', + '---', + 'A `String` type that describes the output value. ', + "The value for the description declaration must be a literal string that's between 0 and 1024 bytes in length. ", + "You can't use a parameter or function to specify the description. ", + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)', + ].join('\n'), + ); + + outputSectionFieldDocsMap.set( + 'Value', + [ + '**Value (required)**', + '\n', + '---', + 'The value of the property returned by the [describe-stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/service_code_examples.html#describe-stacks-sdk) command. ', + 'The value of an output can include literals, parameter references, pseudo parameters, a mapping value, or intrinsic functions. ', + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)', + ].join('\n'), + ); + + outputSectionFieldDocsMap.set( + 'Export', + [ + '**Export (optional)**', + '\n', + '---', + 'The name of the resource output to be exported for a cross-stack reference.', + '\n', + 'You can use intrinsic functions to customize the Name value of an export. ', + '\n', + 'For more information, see [Get exported outputs from a deployed CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html). ', + '\n', + '[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)', + ].join('\n'), + ); + + return outputSectionFieldDocsMap; +} diff --git a/src/hover/HoverRouter.ts b/src/hover/HoverRouter.ts index 1ac10edd..4da1f5c0 100644 --- a/src/hover/HoverRouter.ts +++ b/src/hover/HoverRouter.ts @@ -13,6 +13,7 @@ import { HoverProvider } from './HoverProvider'; import { IntrinsicFunctionArgumentHoverProvider } from './IntrinsicFunctionArgumentHoverProvider'; import { IntrinsicFunctionHoverProvider } from './IntrinsicFunctionHoverProvider'; import { MappingHoverProvider } from './MappingHoverProvider'; +import { OutputSectionFieldHoverProvider } from './OutputSectionFieldHoverProvider'; import { ParameterAttributeHoverProvider } from './ParameterAttributeHoverProvider'; import { ParameterHoverProvider } from './ParameterHoverProvider'; import { PseudoParameterHoverProvider } from './PseudoParameterHoverProvider'; @@ -101,6 +102,11 @@ export class HoverRouter implements Configurable, Closeable { if (doc) { return doc; } + } else if (context.section === TopLevelSection.Outputs && this.isOutputAttribute(context)) { + const doc = this.hoverProviderMap.get(HoverType.OutputSectionField)?.getInformation(context); + if (doc) { + return doc; + } } return this.getTopLevelReference(context); @@ -112,6 +118,7 @@ export class HoverRouter implements Configurable, Closeable { hoverProviderMap.set(HoverType.ResourceSection, new ResourceSectionHoverProvider(schemaRetriever)); hoverProviderMap.set(HoverType.Parameter, new ParameterHoverProvider()); hoverProviderMap.set(HoverType.ParameterAttribute, new ParameterAttributeHoverProvider()); + hoverProviderMap.set(HoverType.OutputSectionField, new OutputSectionFieldHoverProvider()); hoverProviderMap.set(HoverType.PseudoParameter, new PseudoParameterHoverProvider()); hoverProviderMap.set(HoverType.Condition, new ConditionHoverProvider()); hoverProviderMap.set(HoverType.Mapping, new MappingHoverProvider()); @@ -128,6 +135,14 @@ export class HoverRouter implements Configurable, Closeable { return ParameterAttributeHoverProvider.isParameterAttribute(context.text); } + private isOutputAttribute(context: Context): boolean { + if (context.section !== TopLevelSection.Outputs) { + return false; + } + + return OutputSectionFieldHoverProvider.isOutputSectionField(context.text); + } + private getTopLevelReference(context: ContextWithRelatedEntities): string | undefined { for (const section of context.relatedEntities.values()) { const relatedContext = section.get(context.text); @@ -170,6 +185,7 @@ enum HoverType { IntrinsicFunctionArgument, Parameter, ParameterAttribute, + OutputSectionField, PseudoParameter, Condition, Mapping, diff --git a/src/hover/OutputSectionFieldHoverProvider.ts b/src/hover/OutputSectionFieldHoverProvider.ts new file mode 100644 index 00000000..1c49b910 --- /dev/null +++ b/src/hover/OutputSectionFieldHoverProvider.ts @@ -0,0 +1,21 @@ +import { outputSectionFieldDocsMap } from '../artifacts/OutputSectionFieldDocs'; +import { Context } from '../context/Context'; +import { HoverProvider } from './HoverProvider'; + +export class OutputSectionFieldHoverProvider implements HoverProvider { + private static readonly OUTPUT_SECTION_FIELDS = new Set(['Description', 'Value', 'Export']); + + getInformation(context: Context): string | undefined { + const attributeName = context.text; + + if (!OutputSectionFieldHoverProvider.OUTPUT_SECTION_FIELDS.has(attributeName)) { + return undefined; + } + + return outputSectionFieldDocsMap.get(attributeName); + } + + static isOutputSectionField(text: string): boolean { + return OutputSectionFieldHoverProvider.OUTPUT_SECTION_FIELDS.has(text); + } +} diff --git a/tst/e2e/hover/Hover.test.ts b/tst/e2e/hover/Hover.test.ts index ef4d017e..a574ed3f 100644 --- a/tst/e2e/hover/Hover.test.ts +++ b/tst/e2e/hover/Hover.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { intrinsicFunctionsDocsMap } from '../../../src/artifacts/IntrinsicFunctionsDocs'; +import { outputSectionFieldDocsMap } from '../../../src/artifacts/OutputSectionFieldDocs'; import { parameterAttributeDocsMap } from '../../../src/artifacts/ParameterAttributeDocs'; import { pseudoParameterDocsMap } from '../../../src/artifacts/PseudoParameterDocs'; import { resourceAttributeDocsMap } from '../../../src/artifacts/ResourceAttributeDocs'; @@ -1474,6 +1475,30 @@ Outputs:`, .build(), }, }, + { + action: 'type', + content: ``, + position: { line: 408, character: 18 }, + description: 'Test Hover for Description output section field', + verification: { + position: { line: 407, character: 8 }, + expectation: HoverExpectationBuilder.create() + .expectContent(outputSectionFieldDocsMap.get('Description')) + .build(), + }, + }, + { + action: 'type', + content: ``, + position: { line: 408, character: 18 }, + description: 'Test Hover for Value output section field', + verification: { + position: { line: 408, character: 7 }, + expectation: HoverExpectationBuilder.create() + .expectContent(outputSectionFieldDocsMap.get('Value')) + .build(), + }, + }, { action: 'type', content: ` VPC.CidrBlock @@ -1492,6 +1517,18 @@ Outputs:`, .build(), }, }, + { + action: 'type', + content: ``, + position: { line: 414, character: 18 }, + description: 'Test Hover for Export output section field', + verification: { + position: { line: 409, character: 7 }, + expectation: HoverExpectationBuilder.create() + .expectContent(outputSectionFieldDocsMap.get('Export')) + .build(), + }, + }, { action: 'type', content: ` Database.Endpoint.Address diff --git a/tst/unit/hover/OutputSectionFieldHoverProvider.test.ts b/tst/unit/hover/OutputSectionFieldHoverProvider.test.ts new file mode 100644 index 00000000..98bbeedf --- /dev/null +++ b/tst/unit/hover/OutputSectionFieldHoverProvider.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect } from 'vitest'; +import { TopLevelSection } from '../../../src/context/ContextType'; +import { OutputSectionFieldHoverProvider } from '../../../src/hover/OutputSectionFieldHoverProvider'; +import { createMockContext } from '../../utils/MockContext'; + +describe('OutputSectionFieldHoverProvider', () => { + const outputSectionFieldHoverProvider = new OutputSectionFieldHoverProvider(); + + describe('Output Section Field Hover', () => { + it('should return Description documentation when hovering on Description attribute', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'Description', + propertyPath: ['Outputs', 'MyOutput', 'Description'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toContain('**Description (optional)**'); + expect(result).toContain('A `String` type that describes the output value'); + expect(result).toContain('between 0 and 1024 bytes in length'); + expect(result).toContain("You can't use a parameter or function to specify the description"); + }); + + it('should return Value documentation when hovering on Value attribute', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'Value', + propertyPath: ['Outputs', 'MyOutput', 'Value'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toContain('**Value (required)**'); + expect(result).toContain('The value of the property returned by the [describe-stacks]'); + expect(result).toContain( + 'The value of an output can include literals, parameter references, pseudo parameters, a mapping value, or intrinsic functions', + ); + }); + + it('should return Export documentation when hovering on Export attribute', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'Export', + propertyPath: ['Outputs', 'MyOutput', 'Export'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toContain('**Export (optional)**'); + expect(result).toContain('The name of the resource output to be exported for a cross-stack reference'); + expect(result).toContain('You can use intrinsic functions to customize the Name value of an export'); + expect(result).toContain('Get exported outputs from a deployed CloudFormation stack'); + }); + + it('should return undefined for non-output attributes', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'InvalidAttribute', + propertyPath: ['Outputs', 'MyOutput', 'InvalidAttribute'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for empty text', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: '', + propertyPath: ['Outputs', 'MyOutput'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for parameter attributes (case sensitivity)', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'Type', + propertyPath: ['Outputs', 'MyOutput', 'Type'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for resource attributes', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'DependsOn', + propertyPath: ['Outputs', 'MyOutput', 'DependsOn'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + + it('should be case sensitive for attribute names', () => { + const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', { + text: 'description', + propertyPath: ['Outputs', 'MyOutput', 'description'], + }); + + const result = outputSectionFieldHoverProvider.getInformation(mockContext); + + expect(result).toBeUndefined(); + }); + }); + + describe('isOutputSectionField static method', () => { + it('should return true for valid output attributes', () => { + const validAttributes = ['Description', 'Value', 'Export']; + + for (const attribute of validAttributes) { + expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(true); + } + }); + + it('should return false for invalid output attributes', () => { + const invalidAttributes = [ + 'InvalidAttribute', + 'Type', + 'Default', + 'Properties', + 'Condition', + 'DependsOn', + 'description', + 'value', + 'export', + '', + 'Name', // This is a property of Export, not a top-level output attribute + ]; + + for (const attribute of invalidAttributes) { + expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false); + } + }); + + it('should return false for parameter attributes', () => { + const parameterAttributes = [ + 'Type', + 'Default', + 'AllowedValues', + 'AllowedPattern', + 'MinLength', + 'MaxLength', + 'MinValue', + 'MaxValue', + 'NoEcho', + 'ConstraintDescription', + ]; + + for (const attribute of parameterAttributes) { + expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false); + } + }); + + it('should return false for resource attributes', () => { + const resourceAttributes = [ + 'CreationPolicy', + 'DeletionPolicy', + 'UpdatePolicy', + 'UpdateReplacePolicy', + 'Condition', + 'DependsOn', + 'Metadata', + ]; + + for (const attribute of resourceAttributes) { + expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false); + } + }); + }); +});