Skip to content

Add this.exists() and this.existingResource() as an experimental feature#17727

Merged
tsmallig33 merged 20 commits intomainfrom
tasmalligan/AddThisExistsFunction
Nov 17, 2025
Merged

Add this.exists() and this.existingResource() as an experimental feature#17727
tsmallig33 merged 20 commits intomainfrom
tasmalligan/AddThisExistsFunction

Conversation

@tsmallig33
Copy link
Member

@tsmallig33 tsmallig33 commented Aug 1, 2025

Description

Adds this.exists() for use in resource properties. This namespace is scoped as a local symbol to resource bodies

Example Usage

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {
  name: 'mystorageaccount'
  location: 'eastus'
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    publicNetworkAccess: this.exists() ? 'Enabled' : 'Disabled'
  }
}

Checklist

Microsoft Reviewers: Open in CodeFlow

@github-actions
Copy link
Contributor

github-actions bot commented Aug 1, 2025

Test this change out locally with the following install scripts (Action run 19446115486)

VSCode
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-vsix.sh) --run-id 19446115486
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-vsix.ps1) } -RunId 19446115486"
Azure CLI
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-cli.sh) --run-id 19446115486
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-cli.ps1) } -RunId 19446115486"

@github-actions
Copy link
Contributor

github-actions bot commented Aug 1, 2025

Dotnet Test Results

   102 files   -     51     102 suites   - 51   43m 28s ⏱️ - 20m 26s
12 560 tests +     4  12 560 ✅ +     4  0 💤 ±0  0 ❌ ±0 
28 865 runs   - 14 375  28 865 ✅  - 14 375  0 💤 ±0  0 ❌ ±0 

Results for commit b5b4fc6. ± Comparison against base commit c1d5325.

This pull request removes 1962 and adds 678 tests. Note that renamed tests count towards both.

		nestedProp1: 1
		nestedProp2: 2
		prop1: true
		prop2: false
	1
	2
	\$'")
	prop1: true
	prop2: false
…
Bicep.Cli.UnitTests.Services.ReplEnvironmentTests ‑ ShouldSubmitBuffer_terminates_at_expected_point ("var foo = {
")
Bicep.Cli.UnitTests.Services.ReplEnvironmentTests ‑ ShouldSubmitBuffer_terminates_at_expected_point ("var multilineString = '''
Line 1
Line 2
Line 3
'''")
Bicep.Cli.UnitTests.Services.ReplEnvironmentTests ‑ ShouldSubmitBuffer_terminates_at_expected_point ("var outRoleAssignments object[] = union(map(
  filter(varMockedEntraGroupIds, item => !contains(item.uniqueName, 'DevOps')),
  group => {
    principalId: group.groupId
    definition: group.roleToAssign
    relativeScope: ''
    principalType: 'Group'
  }
),[
  {
    principalId: '22222222-2222-2222-2222-222222222222'
    definition: 'Reader'
    relativeScope: ''
    principalType: 'ServicePrincipal'
  }
])")
Bicep.Cli.UnitTests.Services.ReplEnvironmentTests ‑ ShouldSubmitBuffer_terminates_at_expected_point ("var test = {
  abc: 'def' // boo
}")
Bicep.Cli.UnitTests.Services.ReplEnvironmentTests ‑ ShouldSubmitBuffer_terminates_at_expected_point ("var varMockedEntraGroupIds = [
  {
    uniqueName: 'Reader-Group'
    roleToAssign: 'Reader'
    groupId: '11111111-1111-1111-1111-111111111111'
  }
  {
    uniqueName: 'Contributor-Group'
    roleToAssign: 'Contributor'
    groupId: '22222222-2222-2222-2222-222222222222'
  }
  {
    uniqueName: 'DevOps-Group'
    groupId: '33333333-3333-3333-3333-333333333333'
  }
]")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
�ӽ
�0\u0010\u0007��>E�\u0003�\>zU��"��\u0000�=�b��\u0015
��\u000e���R�`~c� �\u000b�1߸fE.��\u0012VFf.��d+4���\u0003\u0006\u0019��d\u0012�E�x3�$=�U��v�o���T�]��\u0014\u0003�j�Z!\u0008�02:���d�\u0006\u000b~q�!Ί�\u001aq��Eמ\u000f�O�Lo���>��w���0� -X
�\u000c��m����?���\u0007ۂ�\u000b%5�k�wT\u0006K�w���S��y��}�\u0003��	\u0007\u0000\u000c\u0000\u0000,"Value cannot be null. (Parameter 'source')")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
�Ի
�0\u0014\u0006��>E�\u0003���
\u001d\u0004\u0007+R\u0005�U�
X�U�
\u0005_�t\u0010�\u0016�^\u0004�9\u0007r��\u0007�{]��NLQ"���G�oؒ���7\u0008W��7\u000c�\u0012B\u0001\u0000��'i�(+]�Q���\u0007�9�U���(I\u0003Ũ�(P�b�f�v\u0000�uu	�<15����iϺڧ>����~�]ѷ�c�\u0001\u0011\u0004\u000b"\u0018�\u0014`)���\u001f%�����\u0017���6Z�6�]�-������q\u001c�\u0019�\u000b<���\u0000\u000c\u0000\u0000,"'7' is an invalid end of a number. Expected a delimiter. Path: $.INVALID_JSON | LineNumber: 0 | BytePositionInLine: 20.")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
��K\u000e�0\u0010\u0006�=\u0005'(3�\u0017,ػ�
���\u0008�P�$ƻ[\u0016&. n\u0010L��d�i��<��~[�C�x� �)gs�@K9�>@�\u000c�\u000b00J\u0011cI?�$#:��&���^?��Ķ��,�hʍ �y.�\u0010*߄�~���նǢs\u0017W�\u001c?��

�T��\u0007�F��߷��S�\u0001��?\u0000\u0014*�D!�H\u0018�H&�<����\u0013DQ\u0014Ekx\u0002C0\u001e�\u0000\u000c\u0000\u0000,"The path: index.json was not found in artifact contents")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0003�ӽ
�0\u0010\u0007��>E�\u0003�\��V��"��\u0000ўX��4\u0011
�\u001b\u0007q���\u0015�o
���\u0010�l���\u001cm��c)�ٔ�wざ���
HC��q�R�\u0010ڿ}�'N��.��Y?Hd����\u0002�\u0016�I!�,\u0013�(�$�6C5�ik����\u0012{�w���^\u000f����h���>7�U�y*	(�
�0�	�J���d�����ɪ��ō�r��\u001a�dF����2�nQ\u0014E��\\u0001���h\u0000\u000c\u0000\u0000,"Value cannot be null. (Parameter 'source')")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0003�Խ
�0\u0010\u0007��>E�\u0003��4\u001fF� 8X�*\u0008�\u0012l�
��V(�\uda98\udcf8���C0�1���\u0010.�ݛj\u001d�(�\u000b�Q9w	�\u001b�I�[�\u001b�\u0015\u0002�F\u0011UB(�p��$-\u001eEi�z�1z� 6ǦL��\u0007%�V\u001ex\u001e� ��|V��]5��nʋ�dQ\�kq˚x�\u0015���V���
����S�\u0011\u0008�\u0002\u0004S \u0010��I�Qv�����\u0004�q�
V��a\u0017:\u000b\��S�dY�e
�\u0005��&�\u0000\u000c\u0000\u0000,"'7' is an invalid end of a number. Expected a delimiter. Path: $.INVALID_JSON | LineNumber: 0 | BytePositionInLine: 20.")
…

♻️ This comment has been updated with latest results.

@tsmallig33 tsmallig33 marked this pull request as ready for review August 1, 2025 20:53
@tsmallig33 tsmallig33 requested a review from Copilot August 4, 2025 14:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces this().exists as an experimental feature for Bicep, allowing developers to conditionally modify resource properties based on whether the resource already exists during deployment. The feature requires explicit enablement through experimental flags and is restricted to resource property contexts only.

Key changes:

  • Adds a new experimental feature flag thisExistsFunction with configuration support
  • Implements the this() function that returns an object with an exists property
  • Restricts usage to resource properties only, with proper validation and error handling

Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/vscode-bicep/schemas/bicepconfig.schema.json Adds schema definition for the new thisExistsFunction experimental feature
src/Bicep.Core/Semantics/Namespaces/AzNamespaceType.cs Implements the this() function with context validation and return type definition
src/Bicep.Core/Semantics/Namespaces/NamespaceProvider.cs Passes feature provider to namespace creation for conditional function registration
src/Bicep.Core/Intermediate/ Adds new ThisFunctionExpression type and visitor pattern support
src/Bicep.Core/Emit/ExpressionConverter.cs Converts this().exists to ARM template targetExists() function
src/Bicep.Core/Features/ Adds feature flag support across the feature provider infrastructure
src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs Adds error message for invalid usage contexts
src/Bicep.Core/Configuration/ExperimentalFeaturesEnabled.cs Extends configuration to include the new experimental feature
Tests and documentation files Comprehensive test coverage and documentation for the new feature
Comments suppressed due to low confidence (1)

@tsmallig33 tsmallig33 requested a review from jeskew August 4, 2025 15:02
@tsmallig33 tsmallig33 changed the title Add this().exists as an experimental feature Add this.exists() as an experimental feature Aug 6, 2025
@tsmallig33 tsmallig33 marked this pull request as draft August 6, 2025 18:31
@tsmallig33 tsmallig33 changed the title Add this.exists() as an experimental feature Add this.exists() and this.existingResource() as an experimental feature Oct 22, 2025
@tsmallig33 tsmallig33 added the do not merge Do not merge this pull request yet. label Oct 22, 2025
@tsmallig33 tsmallig33 marked this pull request as ready for review October 22, 2025 19:52
@tsmallig33 tsmallig33 removed the do not merge Do not merge this pull request yet. label Nov 11, 2025
.Where(decl => decl.NameSource.IsValid && this.builtInNamespaces.ContainsKey(decl.Name))
.Select(reservedSymbol => DiagnosticBuilder.ForPosition(reservedSymbol.NameSource).SymbolicNameCannotUseReservedNamespaceName(reservedSymbol.Name, this.builtInNamespaces.Keys)));

if (this.features.ThisNamespaceEnabled && scope is FileSymbol)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the scope type check meant to avoid raising a diagnostic on the this namespace symbol? I think this would miss cases where for expression introduces a local variable named this, as in:

resource foo 'type@version' = [for this in range(0, 10): { ... }]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, without the scope check, the diagnostic be raised when 'this' is declared inside resource scopes. I tested locally with for loops and found that defining [for this... would take precedence over the 'this' namespace so shouldn't need the diagnostics. I will add a unit test to cover this unless there are other scopes you can think of where this would miss the validation.

Comment on lines +400 to +411
// If it's a function call and no direct match found, check within LocalThisNamespaceSymbol
if (isFunctionCall)
{
var thisNamespace = scope.Declarations
.OfType<LocalThisNamespaceSymbol>()
.FirstOrDefault();

if (thisNamespace?.TryGetNamespaceType() is NamespaceType namespaceType)
{
return namespaceType.MethodResolver.TryGetSymbol(identifierSyntax);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably out of scope for this PR, but I think there's some technical debt here that may complicate other features in this that have been discussed (like a this.parent property for syntactic child resources). Ideally, Bicep should bind to the matching unqualified symbol if one exists. For example, in the following:

func exists() bool => false

resource foo 'type@version' = {
  name: 'foo'
  properties: {
    bar: exists() ? 'fizz' : 'buzz'
  }
}

the exists() function call should bind to the user-defined function even though it's within a resource body because template authors can fully qualify this.exists() to resolve ambiguity, but there's no global::exists()-type syntax to resolve the ambiguity in the other direction.

I would say that imported functions probably should work the same way (i.e., be callable unqualified unless ambiguous, in which case the file-local symbol should take precedence), and they currently do not, so it might make more sense to address this more generically instead of special-casing the this namespace when defined.

A few options here:

  • Leave this section as is with a note to address before this is GA.
  • Back out the change here and require authors to call this.exists() instead of exists()

I can put a topic in the discussions queue to get more input. In any case, I don't think that should block this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this section for now, which will require fully qualified access to this.exists() and this.existingResource()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why is a new kind of symbol needed (instead of reusing BuiltInNamespaceSymbol)? I see that LocalThisNamespaceSymbol also keeps a reference to the declaring resource but may have missed where that is used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BuiltInNamespaceSymbol is not a DeclaredSymbol, so LocalThisNamespaceSymbol is closer to LocalVariableSymbol and is used to provide the correct ThisNamespaceType as the DeclaredSymbol type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it need to be a DeclaredSymbol? this is never declared (unlike local variables) but is implicitly available (like az and sys).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it can be declared in the resource body scope:

DeclareSymbol(thisNamespaceSymbol);

Is there another way you would recommend adding the symbol to the resource body scope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, scopes can only contain DeclaredSymbol instances. It is odd that you have to declare a fake name source that maps back to a keyword rather than a name, but I guess there is no way around it.

I think this is tied to my comment on NameBindingVisitor; symbol resolution right now is relying on some assumptions that have gotten less true over time, and I think it's worth revisiting the design to see how to make it more flexible. Definitely out of scope for this PR.

@tsmallig33 tsmallig33 merged commit def18c5 into main Nov 17, 2025
42 checks passed
@tsmallig33 tsmallig33 deleted the tasmalligan/AddThisExistsFunction branch November 17, 2025 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants