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
50 changes: 50 additions & 0 deletions src/artifacts/OutputSectionFieldDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export const outputSectionFieldDocsMap = getOutputSectionFieldDocsMap();

function getOutputSectionFieldDocsMap(): Map<string, string> {
const outputSectionFieldDocsMap = new Map<string, string>();

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;
}
16 changes: 16 additions & 0 deletions src/hover/HoverRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand All @@ -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);
Expand Down Expand Up @@ -170,6 +185,7 @@ enum HoverType {
IntrinsicFunctionArgument,
Parameter,
ParameterAttribute,
OutputSectionField,
PseudoParameter,
Condition,
Mapping,
Expand Down
21 changes: 21 additions & 0 deletions src/hover/OutputSectionFieldHoverProvider.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
37 changes: 37 additions & 0 deletions tst/e2e/hover/Hover.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
173 changes: 173 additions & 0 deletions tst/unit/hover/OutputSectionFieldHoverProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
});
Loading