src/Bicep.LangServer/Handlers/BicepDefinitionHandler.cs (472 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
using Azure.Deployments.Core.Definitions.Schema;
using Azure.Deployments.Core.Entities;
using Azure.Deployments.Templates.Extensions;
using Bicep.Core;
using Bicep.Core.Emit;
using Bicep.Core.Extensions;
using Bicep.Core.Features;
using Bicep.Core.FileSystem;
using Bicep.Core.Modules;
using Bicep.Core.Navigation;
using Bicep.Core.Parsing;
using Bicep.Core.Registry;
using Bicep.Core.Registry.Oci;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Metadata;
using Bicep.Core.SourceGraph;
using Bicep.Core.SourceLink;
using Bicep.Core.Syntax;
using Bicep.Core.TypeSystem;
using Bicep.Core.Utils;
using Bicep.IO.Abstraction;
using Bicep.LanguageServer.CompilationManager;
using Bicep.LanguageServer.Completions;
using Bicep.LanguageServer.Extensions;
using Bicep.LanguageServer.Providers;
using Bicep.LanguageServer.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
namespace Bicep.LanguageServer.Handlers
{
public class BicepDefinitionHandler : DefinitionHandlerBase
{
private readonly ISymbolResolver symbolResolver;
private readonly ICompilationManager compilationManager;
private readonly ILanguageServerFacade languageServer;
private readonly IModuleDispatcher moduleDispatcher;
private readonly DocumentSelectorFactory documentSelectorFactory;
public BicepDefinitionHandler(
ISymbolResolver symbolResolver,
ICompilationManager compilationManager,
ILanguageServerFacade languageServer,
IModuleDispatcher moduleDispatcher,
DocumentSelectorFactory documentSelectorFactory) : base()
{
this.symbolResolver = symbolResolver;
this.compilationManager = compilationManager;
this.languageServer = languageServer;
this.moduleDispatcher = moduleDispatcher;
this.documentSelectorFactory = documentSelectorFactory;
}
public override async Task<LocationOrLocationLinks?> Handle(DefinitionParams request, CancellationToken cancellationToken)
{
await Task.CompletedTask;
var context = this.compilationManager.GetCompilation(request.TextDocument.Uri);
if (context is null)
{
return null;
}
var result = this.symbolResolver.ResolveSymbol(request.TextDocument.Uri, request.Position);
// No parent Symbol: ad hoc syntax matching
return result switch
{
null => HandleUnboundSymbolLocation(request, context),
{ Symbol: ParameterAssignmentSymbol param } => HandleParameterAssignment(request, result, context, param),
// Used for the declaration ONLY of a wildcard import. Other syntax that resolves to a wildcard import will be handled by HandleDeclaredDefinitionLocation
{ Origin: WildcardImportSyntax, Symbol: WildcardImportSymbol wildcardImport }
=> HandleWildcardImportDeclaration(context, wildcardImport),
{ Symbol: ImportedSymbol imported } => HandleImportedSymbolLocation(result, context, imported),
{ Symbol: WildcardImportInstanceFunctionSymbol instanceFunctionSymbol }
=> HandleWildcardImportInstanceFunctionLocation(result, context, instanceFunctionSymbol),
{ Symbol: DeclaredSymbol declaration } => HandleDeclaredDefinitionLocation(request, result, declaration),
// Object property: currently only used for module param goto
{ Origin: ObjectPropertySyntax } => HandleObjectPropertyLocation(request, context),
// Used for module (name), variable, wildcard import, or resource property access
{ Symbol: PropertySymbol } => HandlePropertyLocation(request, result, context),
_ => null,
};
}
protected override DefinitionRegistrationOptions CreateRegistrationOptions(DefinitionCapability capability, ClientCapabilities clientCapabilities) => new()
{
DocumentSelector = documentSelectorFactory.CreateForBicepAndParams()
};
private LocationOrLocationLinks HandleUnboundSymbolLocation(DefinitionParams request, CompilationContext context)
{
int offset = PositionHelper.GetOffset(context.LineStarts, request.Position);
var matchingNodes = SyntaxMatcher.FindNodesMatchingOffset(context.Compilation.SourceFileGrouping.EntryPoint.ProgramSyntax, offset);
{ // Definition handler for a non symbol bound to implement module path goto.
// try to resolve module path syntax from given offset using tail matching.
if (SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax, StringSyntax, Token>(
matchingNodes,
(moduleSyntax, stringSyntax, token) => moduleSyntax.Path == stringSyntax && token.Type == TokenType.StringComplete)
&& matchingNodes[^3] is ModuleDeclarationSyntax moduleDeclarationSyntax
&& matchingNodes[^2] is StringSyntax stringToken
&& context.Compilation.SourceFileGrouping.TryGetSourceFile(moduleDeclarationSyntax).IsSuccess(out var sourceFile)
&& this.moduleDispatcher.TryGetArtifactReference(context.Compilation.SourceFileGrouping.EntryPoint, moduleDeclarationSyntax).IsSuccess(out var moduleReference))
{
return HandleModuleReference(context, stringToken, sourceFile, moduleReference);
}
}
{ // Definition handler for a non symbol bound to implement import path goto.
// try to resolve import path syntax from given offset using tail matching.
if (SyntaxMatcher.IsTailMatch<CompileTimeImportDeclarationSyntax, CompileTimeImportFromClauseSyntax, StringSyntax, Token>(
matchingNodes,
(_, fromClauseSyntax, stringSyntax, token) => fromClauseSyntax.Path == stringSyntax && token.Type == TokenType.StringComplete)
&& matchingNodes[^4] is CompileTimeImportDeclarationSyntax importDeclarationSyntax
&& matchingNodes[^2] is StringSyntax stringToken
&& context.Compilation.SourceFileGrouping.TryGetSourceFile(importDeclarationSyntax).IsSuccess(out var sourceFile)
&& this.moduleDispatcher.TryGetArtifactReference(context.Compilation.SourceFileGrouping.EntryPoint, importDeclarationSyntax).IsSuccess(out var moduleReference))
{
// goto beginning of the module file.
return GetFileDefinitionLocation(
GetModuleSourceLinkUri(sourceFile, moduleReference),
stringToken,
context,
new() { Start = new(0, 0), End = new(0, 0) });
}
}
{ // Definition handler for a non symbol bound to implement load* functions file argument path goto.
if (SyntaxMatcher.IsTailMatch<StringSyntax, Token>(
matchingNodes,
(stringSyntax, token) => !stringSyntax.IsInterpolated() && token.Type == TokenType.StringComplete)
&& matchingNodes[^2] is StringSyntax stringToken
&& context.Compilation.GetEntrypointSemanticModel().GetDeclaredType(stringToken) is { } stringType
&& stringType.ValidationFlags.HasFlag(TypeSymbolValidationFlags.IsStringFilePath)
&& stringToken.TryGetLiteralValue() is { } stringTokenValue
&& RelativePath.TryCreate(stringTokenValue).Transform(context.Compilation.SourceFileGrouping.EntryPoint.FileHandle.TryGetRelativeFile).IsSuccess(out var relativeFile)
&& relativeFile.Exists())
{
return GetFileDefinitionLocation(
relativeFile.Uri.ToUri(),
stringToken,
context,
new() { Start = new(0, 0), End = new(0, 0) });
}
}
{
if (SyntaxMatcher.GetTailMatch<UsingDeclarationSyntax, StringSyntax, Token>(matchingNodes) is (var @using, var path, _) &&
@using.Path == path &&
context.Compilation.SourceFileGrouping.TryGetSourceFile(@using).IsSuccess(out var sourceFile))
{
return GetFileDefinitionLocation(
sourceFile.Uri,
path,
context,
new() { Start = new(0, 0), End = new(0, 0) });
}
}
// all other unbound syntax nodes return no
return new();
}
private LocationOrLocationLinks HandleModuleReference(CompilationContext context, StringSyntax stringToken, ISourceFile sourceFile, ArtifactReference reference)
{
// Return the correct link format so our language client can display the sources
return GetFileDefinitionLocation(
GetModuleSourceLinkUri(sourceFile, reference),
stringToken,
context,
new() { Start = new(0, 0), End = new(0, 0) });
}
private Uri GetModuleSourceLinkUri(ISourceFile sourceFile, ArtifactReference reference)
{
if (!this.CanClientAcceptRegistryContent() || !reference.IsExternal)
{
// the client doesn't support the bicep-extsrc scheme or we're dealing with a local module
// just use the file URI
return sourceFile.Uri;
}
if (reference is OciArtifactReference ociArtifactReference)
{
return BicepExternalSourceRequestHandler.GetRegistryModuleSourceLinkUri(ociArtifactReference, ociArtifactReference.TryLoadSourceArchive().TryUnwrap());
}
if (reference is TemplateSpecModuleReference templateSpecModuleReference)
{
return BicepExternalSourceRequestHandler.GetTemplateSpecSourceLinkUri(templateSpecModuleReference);
}
throw new UnreachableException();
}
private LocationOrLocationLinks HandleWildcardImportDeclaration(CompilationContext context, WildcardImportSymbol wildcardImport)
{
if (context.Compilation.SourceFileGrouping.TryGetSourceFile(wildcardImport.EnclosingDeclaration).IsSuccess(out var sourceFile) &&
this.moduleDispatcher.TryGetArtifactReference(context.Compilation.SourceFileGrouping.EntryPoint, wildcardImport.EnclosingDeclaration).IsSuccess(out var moduleReference))
{
return GetFileDefinitionLocation(
GetModuleSourceLinkUri(sourceFile, moduleReference),
wildcardImport.DeclaringSyntax,
context,
new() { Start = new(0, 0), End = new(0, 0) });
}
return new();
}
private static LocationOrLocationLinks HandleDeclaredDefinitionLocation(DefinitionParams request, SymbolResolutionResult result, DeclaredSymbol declaration)
{
return new(new LocationOrLocationLink(new LocationLink
{
// source of the link. Underline only the symbolic name
OriginSelectionRange = (result.Origin is ITopLevelNamedDeclarationSyntax named ? named.Name : result.Origin).ToRange(result.Context.LineStarts),
TargetUri = request.TextDocument.Uri,
// entire span of the declaredSymbol
TargetRange = declaration.DeclaringSyntax.ToRange(result.Context.LineStarts),
TargetSelectionRange = declaration.NameSource.ToRange(result.Context.LineStarts)
}));
}
private LocationOrLocationLinks HandleObjectPropertyLocation(DefinitionParams request, CompilationContext context)
{
int offset = PositionHelper.GetOffset(context.LineStarts, request.Position);
var matchingNodes = SyntaxMatcher.FindNodesMatchingOffset(context.Compilation.SourceFileGrouping.EntryPoint.ProgramSyntax, offset);
// matchingNodes[0] should be ProgramSyntax
if (matchingNodes[1] is ModuleDeclarationSyntax moduleDeclarationSyntax)
{
// capture the property accesses leading to this specific property access
var propertyAccesses = matchingNodes.OfType<ObjectPropertySyntax>().ToList();
// only two level of traversals: mod { params: { <outputName1>: ...}}
if (propertyAccesses.Count == 2 &&
propertyAccesses[0].TryGetKeyText() is { } propertyType &&
propertyAccesses[1].TryGetKeyText() is { } propertyName)
{
// underline only the key of the object property access
return GetModuleSymbolLocation(
propertyAccesses.Last().Key,
context,
moduleDeclarationSyntax,
propertyType,
propertyName);
}
}
return new();
}
private LocationOrLocationLinks HandlePropertyLocation(DefinitionParams request, SymbolResolutionResult result, CompilationContext context)
{
var semanticModel = context.Compilation.GetEntrypointSemanticModel();
// Find the underlying VariableSyntax being accessed
var syntax = result.Origin;
var propertyAccesses = new List<IdentifierSyntax>();
while (true)
{
if (syntax is PropertyAccessSyntax propertyAccessSyntax)
{
// since we are traversing bottom up, add this access to the beginning of the list
propertyAccesses.Insert(0, propertyAccessSyntax.PropertyName);
syntax = propertyAccessSyntax.BaseExpression;
continue;
}
if (syntax is TypePropertyAccessSyntax typePropertyAccessSyntax)
{
// since we are traversing bottom up, add this access to the beginning of the list
propertyAccesses.Insert(0, typePropertyAccessSyntax.PropertyName);
syntax = typePropertyAccessSyntax.BaseExpression;
continue;
}
if (syntax is ParenthesizedExpressionSyntax parenthesized)
{
syntax = parenthesized.Expression;
continue;
}
break;
}
if (syntax is VariableAccessSyntax or TypeVariableAccessSyntax
&& semanticModel.GetSymbolInfo(syntax) is DeclaredSymbol ancestorSymbol)
{
// If the symbol is a module, we need to redirect the user to the module file
// note: module.name doesn't follow this: it should refer to the declaration of the module in the current file, like regular variable and resource property accesses
if (propertyAccesses.Count == 2
&& ancestorSymbol.DeclaringSyntax is ModuleDeclarationSyntax moduleDeclarationSyntax)
{
// underline only the last property access
return GetModuleSymbolLocation(
propertyAccesses.Last(),
context,
moduleDeclarationSyntax,
propertyAccesses[0].IdentifierName,
propertyAccesses[1].IdentifierName);
}
// The user should be redirected to the import target file if the symbol is a wildcard import
if (propertyAccesses.Count == 1 && ancestorSymbol is WildcardImportSymbol wildcardImport)
{
return HandleImportedSymbolLocation(result.Origin.ToRange(context.LineStarts), context, wildcardImport.SourceModel, propertyAccesses.Single().IdentifierName, wildcardImport.EnclosingDeclaration);
}
// Otherwise, we redirect user to the specified module, variable, or resource declaration
if (GetObjectSyntaxFromDeclaration(ancestorSymbol.DeclaringSyntax) is ObjectSyntax objectSyntax
&& ObjectSyntaxExtensions.TryGetPropertyByNameRecursive(objectSyntax, propertyAccesses) is ObjectPropertySyntax resultingSyntax)
{
// underline only the last property access
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = propertyAccesses.Last().ToRange(result.Context.LineStarts),
TargetUri = request.TextDocument.Uri,
TargetRange = resultingSyntax.ToRange(result.Context.LineStarts),
TargetSelectionRange = resultingSyntax.ToRange(result.Context.LineStarts)
}));
}
}
return new();
}
private LocationOrLocationLinks HandleParameterAssignment(DefinitionParams request, SymbolResolutionResult result, CompilationContext context, ParameterAssignmentSymbol param)
{
if (param.NameSource is not { } nameSyntax)
{
return new();
}
var paramsModel = context.Compilation.GetEntrypointSemanticModel();
if (!paramsModel.Root.TryGetBicepFileSemanticModelViaUsing().IsSuccess(out var usingModel) ||
usingModel is not SemanticModel bicepModel)
{
return new();
}
if (bicepModel.Root.ParameterDeclarations
.FirstOrDefault(x => x.DeclaringParameter.Name.NameEquals(param.Name)) is not ParameterSymbol parameterSymbol)
{
return new();
}
var range = PositionHelper.GetNameRange(bicepModel.SourceFile.LineStarts, parameterSymbol.DeclaringSyntax);
var documentUri = bicepModel.SourceFile.Uri;
return new(new LocationOrLocationLink(new LocationLink
{
// source of the link. Underline only the symbolic name
OriginSelectionRange = nameSyntax.ToRange(context.LineStarts),
TargetUri = documentUri,
// entire span of the declaredSymbol
TargetRange = range,
TargetSelectionRange = range
}));
}
private static LocationOrLocationLinks HandleImportedSymbolLocation(SymbolResolutionResult result, CompilationContext context, ImportedSymbol imported)
=> HandleImportedSymbolLocation(result.Origin.ToRange(context.LineStarts), context, imported.SourceModel, imported.OriginalSymbolName, imported.EnclosingDeclaration);
private static LocationOrLocationLinks HandleWildcardImportInstanceFunctionLocation(SymbolResolutionResult result, CompilationContext context, WildcardImportInstanceFunctionSymbol symbol)
=> HandleImportedSymbolLocation(result.Origin.ToRange(context.LineStarts), context, symbol.BaseSymbol.SourceModel, symbol.Name, symbol.BaseSymbol.EnclosingDeclaration);
private static LocationOrLocationLinks HandleImportedSymbolLocation(Range originSelectionRange, CompilationContext context, ISemanticModel sourceModel, string? originalSymbolName, IArtifactReferenceSyntax enclosingDeclaration)
{
if (sourceModel is SemanticModel bicepModel &&
bicepModel.Root.Declarations.Where(type => LanguageConstants.IdentifierComparer.Equals(type.Name, originalSymbolName)).FirstOrDefault() is { } originalDeclaration)
{
// entire span of the declaredSymbol
var targetRange = PositionHelper.GetNameRange(bicepModel.SourceFile.LineStarts, originalDeclaration.DeclaringSyntax);
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originSelectionRange,
TargetUri = bicepModel.SourceFile.Uri,
TargetRange = targetRange,
TargetSelectionRange = targetRange,
}));
}
var (armTemplate, armTemplateUri) = GetArmSourceTemplateInfo(context, enclosingDeclaration);
if (armTemplateUri is not null && originalSymbolName is string nonNullName && sourceModel.Exports.TryGetValue(nonNullName, out var exportMetadata))
{
if (exportMetadata.Kind == ExportMetadataKind.Type &&
armTemplate?.Definitions?.TryGetValue(nonNullName, out var originalTypeDefinition) is true &&
ToRange(originalTypeDefinition) is Range typeDefinitionRange)
{
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originSelectionRange,
TargetUri = armTemplateUri,
TargetRange = typeDefinitionRange,
TargetSelectionRange = typeDefinitionRange,
}));
}
if (exportMetadata.Kind == ExportMetadataKind.Variable)
{
if (armTemplate?.Variables?.TryGetValue(nonNullName, out var variableDeclaration) is true && ToRange(variableDeclaration) is Range variableDefinitionRange)
{
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originSelectionRange,
TargetUri = armTemplateUri,
TargetRange = variableDefinitionRange,
TargetSelectionRange = variableDefinitionRange,
}));
}
if (armTemplate?.Variables?.TryGetValue("copy", out var copyVariablesDeclaration) is true &&
copyVariablesDeclaration.Value is JArray copyVariablesArray &&
copyVariablesArray.Where(e => e is JObject objectElement &&
objectElement.TryGetValue("name", StringComparison.OrdinalIgnoreCase, out var nameToken) &&
nameToken is JValue { Value: string nameString } &&
StringComparer.OrdinalIgnoreCase.Equals(nameString, nonNullName))
.FirstOrDefault() is JToken copyVariableToken &&
ToRange(copyVariableToken) is Range copyVariableDefinitionRange)
{
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originSelectionRange,
TargetUri = armTemplateUri,
TargetRange = copyVariableDefinitionRange,
TargetSelectionRange = copyVariableDefinitionRange,
}));
}
}
if (exportMetadata.Kind == ExportMetadataKind.Function)
{
var fullyQualifiedFunctionName = nonNullName.Contains('.')
? nonNullName
: $"{EmitConstants.UserDefinedFunctionsNamespace}.{nonNullName}";
if (armTemplate?.GetFunctionDefinitions()
.Where(fd => StringComparer.OrdinalIgnoreCase.Equals(fd.Key, fullyQualifiedFunctionName))
.FirstOrDefault() is FunctionDefinition functionDefinition &&
ToRange(functionDefinition.Function) is Range functionDefinitionRange)
{
return new(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originSelectionRange,
TargetUri = armTemplateUri,
TargetRange = functionDefinitionRange,
TargetSelectionRange = functionDefinitionRange,
}));
}
}
}
return new();
}
private static (Template?, Uri?) GetArmSourceTemplateInfo(CompilationContext context, IArtifactReferenceSyntax foreignTemplateReference)
=> context.Compilation.SourceFileGrouping.TryGetSourceFile(foreignTemplateReference).TryUnwrap() switch
{
TemplateSpecFile templateSpecFile => (templateSpecFile.MainTemplateFile.Template, templateSpecFile.Uri),
ArmTemplateFile armTemplateFile => (armTemplateFile.Template, armTemplateFile.Uri),
_ => (null, null),
};
private static Range? ToRange(JTokenMetadata jToken)
=> jToken.LineNumber.HasValue && jToken.LinePosition.HasValue
? new(jToken.LineNumber.Value - 1, jToken.LinePosition.Value, jToken.LineNumber.Value - 1, jToken.LinePosition.Value)
: null;
private static Range? ToRange(IJsonLineInfo jsonLineInfo)
=> jsonLineInfo.LineNumber > 0
? new(jsonLineInfo.LineNumber - 1, jsonLineInfo.LinePosition, jsonLineInfo.LineNumber - 1, jsonLineInfo.LinePosition)
: null;
private LocationOrLocationLinks GetModuleSymbolLocation(
SyntaxBase underlinedSyntax,
CompilationContext context,
ModuleDeclarationSyntax moduleDeclarationSyntax,
string propertyType,
string propertyName)
{
if (context.Compilation.SourceFileGrouping.TryGetSourceFile(moduleDeclarationSyntax).IsSuccess(out var sourceFile) && sourceFile is BicepFile bicepFile
&& context.Compilation.GetSemanticModel(bicepFile) is SemanticModel moduleModel)
{
switch (propertyType)
{
case LanguageConstants.ModuleOutputsPropertyName:
if (moduleModel.Root.OutputDeclarations
.FirstOrDefault(d => string.Equals(d.Name, propertyName)) is OutputSymbol outputSymbol)
{
return GetFileDefinitionLocation(
bicepFile.Uri,
underlinedSyntax,
context,
outputSymbol.DeclaringOutput.Name.ToRange(bicepFile.LineStarts));
}
break;
case LanguageConstants.ModuleParamsPropertyName:
if (moduleModel.Root.ParameterDeclarations
.FirstOrDefault(d => string.Equals(d.Name, propertyName)) is ParameterSymbol parameterSymbol)
{
return GetFileDefinitionLocation(
bicepFile.Uri,
underlinedSyntax,
context,
parameterSymbol.DeclaringParameter.Name.ToRange(bicepFile.LineStarts));
}
break;
}
}
return new();
}
private static LocationOrLocationLinks GetFileDefinitionLocation(
Uri fileUri,
SyntaxBase originalSelectionSyntax,
CompilationContext context,
Range targetRange)
{
return new LocationOrLocationLinks(new LocationOrLocationLink(new LocationLink
{
OriginSelectionRange = originalSelectionSyntax.ToRange(context.LineStarts),
TargetUri = DocumentUri.From(fileUri),
TargetRange = targetRange,
TargetSelectionRange = targetRange
}));
}
private static ObjectSyntax? GetObjectSyntaxFromDeclaration(SyntaxBase syntax) => syntax switch
{
ResourceDeclarationSyntax resourceDeclarationSyntax when resourceDeclarationSyntax.TryGetBody() is ObjectSyntax objectSyntax => objectSyntax,
ModuleDeclarationSyntax moduleDeclarationSyntax when moduleDeclarationSyntax.TryGetBody() is ObjectSyntax objectSyntax => objectSyntax,
VariableDeclarationSyntax variableDeclarationSyntax when variableDeclarationSyntax.Value is ObjectSyntax objectSyntax => objectSyntax,
_ => null,
};
// True if the client knows how (like our vscode extension) to handle the "bicep-extsrc:" schema
private bool CanClientAcceptRegistryContent()
{
if (this.languageServer.ClientSettings.InitializationOptions is not JObject obj ||
obj.Property("enableRegistryContent") is not { } property ||
property.Value.Type != JTokenType.Boolean)
{
return false;
}
return property.Value.Value<bool>();
}
}
}