-
Notifications
You must be signed in to change notification settings - Fork 819
Expand file tree
/
Copy pathFunctionPlacementValidatorVisitor.cs
More file actions
151 lines (129 loc) · 8.14 KB
/
FunctionPlacementValidatorVisitor.cs
File metadata and controls
151 lines (129 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Bicep.Core.Diagnostics;
using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using Bicep.Core.TypeSystem;
using Bicep.Core.Utils;
namespace Bicep.Core.Emit
{
public class FunctionPlacementValidatorVisitor : AstVisitor
{
private enum VisitedElement
{
Module,
ModuleParams,
ModuleExtensionConfigs
}
private readonly SemanticModel semanticModel;
private readonly IDiagnosticWriter diagnosticWriter;
private readonly VisitorRecorder<(SyntaxBase syntax, Symbol? symbol)> syntaxRecorder = new();
private readonly VisitorRecorder<VisitedElement> elementsRecorder = new();
private FunctionPlacementValidatorVisitor(SemanticModel semanticModel, IDiagnosticWriter diagnosticWriter)
{
this.semanticModel = semanticModel;
this.diagnosticWriter = diagnosticWriter;
}
public static void Validate(SemanticModel semanticModel, IDiagnosticWriter diagnosticWriter)
{
var visitor = new FunctionPlacementValidatorVisitor(semanticModel, diagnosticWriter);
// visiting writes diagnostics in some cases
visitor.Visit(semanticModel.SourceFile.ProgramSyntax);
}
protected override void VisitInternal(SyntaxBase node)
{
using var _ = syntaxRecorder.Scope((node, semanticModel.GetSymbolInfo(node)));
base.VisitInternal(node);
}
public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax)
{
if (semanticModel.GetTypeInfo(syntax).IsError())
{
return; //suppressing checking this module. it couldn't be read therefore diagnostics emitted might be misleading
}
using var _ = elementsRecorder.Scope(VisitedElement.Module);
base.VisitModuleDeclarationSyntax(syntax);
}
public override void VisitObjectPropertySyntax(ObjectPropertySyntax syntax)
{
VisitedElement? scope = null;
if (elementsRecorder.Contains(VisitedElement.Module) && syntax.Key is IdentifierSyntax identifierSyntax && elementsRecorder.TryPeek(out var head))
{
if (head != VisitedElement.ModuleParams && string.Equals(identifierSyntax.IdentifierName, LanguageConstants.ModuleParamsPropertyName, LanguageConstants.IdentifierComparison))
{
scope = VisitedElement.ModuleParams;
}
else if (head != VisitedElement.ModuleExtensionConfigs && string.Equals(identifierSyntax.IdentifierName, LanguageConstants.ModuleExtensionConfigsPropertyName, LanguageConstants.IdentifierComparison))
{
scope = VisitedElement.ModuleExtensionConfigs;
}
}
using var _ = scope is not null ? elementsRecorder.Scope(scope.Value) : null;
base.VisitObjectPropertySyntax(syntax);
}
public override void VisitInstanceFunctionCallSyntax(InstanceFunctionCallSyntax syntax)
{
VerifyModuleSecureParameterFunctionPlacement(syntax);
base.VisitInstanceFunctionCallSyntax(syntax);
}
public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax)
{
VerifyModuleSecureParameterFunctionPlacement(syntax);
base.VisitFunctionCallSyntax(syntax);
}
private void VerifyModuleSecureParameterFunctionPlacement(FunctionCallSyntaxBase syntax)
{
if (semanticModel.GetSymbolInfo(syntax) is FunctionSymbol functionSymbol)
{
if (functionSymbol.FunctionFlags.HasFlag(FunctionFlags.ModuleSecureParameterOnly))
{
// we can check placement only for functions that were matched and has a proper placement flag
var (_, levelUpSymbol) = syntaxRecorder.Skip(1).SkipWhile(x => x.syntax is TernaryOperationSyntax).FirstOrDefault();
// Check if getSecret is nested inside an object structure (not a direct child of params/extensionConfigs)
// Invalid for params: params: { config: { secret: kv.getSecret(...) } } <- ObjectSyntax between function and parameter property
// Valid for params: params: { secret: kv.getSecret(...) } <- No ObjectSyntax between
// Valid for params: params: { secret: cond ? kv.getSecret(...) : 'x' } <- Ternaries are skipped
// Valid for extensionConfigs: extensionConfigs: { alias: { prop: kv.getSecret(...) } } <- 1 ObjectSyntax (alias) is OK
// Invalid for extensionConfigs: extensionConfigs: { alias: { obj: { prop: kv.getSecret(...) } } } <- 2+ ObjectSyntax is invalid
// Count ObjectSyntax nodes between the immediate property and the params/extensionConfigs value object
// The params value object is the ObjectSyntax that immediately follows the params ObjectPropertySyntax
var ancestors = syntaxRecorder
.Skip(1) // Skip the function call
.SkipWhile(x => x.syntax is TernaryOperationSyntax) // Skip ternary operators
.Skip(1) // Skip the immediate ObjectPropertySyntax (the property containing getSecret, e.g., mySecret, certificate)
.ToList();
// Find the params/extensionConfigs property
var paramsPropertyIndex = ancestors.FindIndex(x =>
x.syntax is ObjectPropertySyntax ops &&
ops.TryGetKeyText() is string key &&
(string.Equals(key, LanguageConstants.ModuleParamsPropertyName, LanguageConstants.IdentifierComparison) ||
string.Equals(key, LanguageConstants.ModuleExtensionConfigsPropertyName, LanguageConstants.IdentifierComparison)));
// Count ObjectSyntax nodes before the params property (excluding the params value object)
// Stop one element BEFORE the params property (since the element before params property is the params value object)
var objectSyntaxCount = paramsPropertyIndex >= 1
? ancestors.Take(paramsPropertyIndex - 1).Count(x => x.syntax is ObjectSyntax)
: 0;
var isInExtensionConfigs = elementsRecorder.TryPeek(out var head) && head is VisitedElement.ModuleExtensionConfigs;
var maxAllowedNesting = isInExtensionConfigs ? 1 : 0; // Extension configs allow 1 level (the alias), params allow 0
var isNestedInObject = levelUpSymbol is PropertySymbol && objectSyntaxCount > maxAllowedNesting;
if (!(elementsRecorder.TryPeek(out head) && head is VisitedElement.ModuleParams or VisitedElement.ModuleExtensionConfigs)
|| levelUpSymbol is not PropertySymbol propertySymbol
|| !(TypeHelper.TryRemoveNullability(propertySymbol.Type) ?? propertySymbol.Type).ValidationFlags.HasFlag(TypeSymbolValidationFlags.IsSecure)
|| isNestedInObject)
{
diagnosticWriter.Write(DiagnosticBuilder.ForPosition(syntax)
.FunctionOnlyValidInModuleSecureParameterAndExtensionConfigAssignment(functionSymbol.Name, semanticModel.Features.ModuleExtensionConfigsEnabled));
}
}
if (functionSymbol.FunctionFlags.HasFlag(FunctionFlags.DirectAssignment))
{
var (_, levelUpSymbol) = syntaxRecorder.Skip(1).SkipWhile(x => x.syntax is TernaryOperationSyntax).FirstOrDefault();
if (levelUpSymbol is null)
{
diagnosticWriter.Write(DiagnosticBuilder.ForPosition(syntax).FunctionOnlyValidWithDirectAssignment(functionSymbol.Name));
}
}
}
}
}
}