├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Directory.Build.props ├── Directory.Build.targets ├── GitVersion.yml ├── LICENSE ├── MSBuildProjectTools.ruleset ├── MSBuildProjectTools.sln ├── OSSREADME.json ├── README.md ├── azure-pipelines.yml ├── build.ps1 ├── build.sh ├── dist.sh ├── docs ├── BUILDING.md └── architecture │ ├── diagrams.pptx │ ├── images │ ├── lsp-layers.png │ └── server-layers.png │ └── overview.md ├── global.json ├── help ├── elements.json ├── items.json ├── properties.json └── tasks.json ├── src ├── Directory.Build.props ├── LanguageServer.Common │ ├── Help │ │ ├── ElementHelp.cs │ │ ├── ItemHelp.cs │ │ ├── PropertyHelp.cs │ │ └── TaskHelp.cs │ ├── LanguageServer.Common.csproj │ ├── Position.cs │ ├── Range.cs │ └── Utilities │ │ ├── ActivityCorrelationManager.cs │ │ ├── DotnetInfo.cs │ │ ├── LinqHelper.cs │ │ ├── MSBuildHelper.cs │ │ ├── NuGetHelper.cs │ │ ├── NuGetResourceProviderVersions.cs │ │ ├── TextPositions.cs │ │ └── VSCodeDocumentUri.cs ├── LanguageServer.Engine │ ├── CompletionProviders │ │ ├── CommentCompletionProvider.cs │ │ ├── CompletionProvider.cs │ │ ├── ICompletionProvider.cs │ │ ├── ItemAttributeCompletionProvider.cs │ │ ├── ItemElementCompletionProvider.cs │ │ ├── ItemGroupExpressionCompletionProvider.cs │ │ ├── ItemMetadataCompletionProvider.cs │ │ ├── ItemMetadataExpressionCompletionProvider.cs │ │ ├── PackageReferenceCompletionProvider.cs │ │ ├── PropertyConditionCompletionProvider.cs │ │ ├── PropertyElementCompletionProvider.cs │ │ ├── PropertyExpressionCompletionProvider.cs │ │ ├── TargetNameCompletionProvider.cs │ │ ├── TaskCompletionProvider.cs │ │ ├── TaskElementCompletionProvider.cs │ │ ├── TaskParameterCompletionProvider.cs │ │ └── TopLevelElementCompletionProvider.cs │ ├── Configuration.cs │ ├── ContentProviders │ │ └── HoverContentProvider.cs │ ├── CustomProtocol │ │ ├── BusyNotification.cs │ │ ├── CustomCompletionHandler.cs │ │ ├── DidChangeConfigurationSettings.cs │ │ └── ProtocolExtensions.cs │ ├── Diagnostics │ │ ├── IPublishDiagnostics.cs │ │ └── LspDiagnosticsPublisher.cs │ ├── Documents │ │ ├── MasterProjectDocument.cs │ │ ├── ProjectDocument.cs │ │ ├── ProjectDocumentKind.cs │ │ ├── SubProjectDocument.cs │ │ └── Workspace.cs │ ├── Handlers │ │ ├── CompletionHandler.cs │ │ ├── ConfigurationHandler.cs │ │ ├── DefinitionHandler.cs │ │ ├── DocumentSymbolHandler.cs │ │ ├── DocumentSyncHandler.cs │ │ ├── Handler.cs │ │ └── HoverHandler.cs │ ├── LanguageServer.Engine.csproj │ ├── LanguageServerModule.cs │ ├── Logging │ │ ├── ActivityIdEnricher.cs │ │ ├── LanguageServerSink.cs │ │ └── SerilogLanguageServerExtensions.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ └── Utilities │ │ ├── CollectionExtensions.cs │ │ ├── ModelConversions.cs │ │ └── UsingTaskAssemblyEqualityComparer.cs ├── LanguageServer.SemanticModel.MSBuild │ ├── ExpressionExtensions.cs │ ├── LanguageServer.SemanticModel.MSBuild.csproj │ ├── MSBuildExceptionExtensions.cs │ ├── MSBuildExpression.cs │ ├── MSBuildExpressions │ │ ├── Compare.cs │ │ ├── ComparisonKind.cs │ │ ├── EmptyListItem.cs │ │ ├── Evaluate.cs │ │ ├── ExpressionContainerNode.cs │ │ ├── ExpressionHelper.cs │ │ ├── ExpressionKind.cs │ │ ├── ExpressionList.cs │ │ ├── ExpressionNode.cs │ │ ├── ExpressionTree.cs │ │ ├── FunctionCall.cs │ │ ├── FunctionKind.cs │ │ ├── ItemGroup.cs │ │ ├── ItemGroupTransform.cs │ │ ├── ItemMetadata.cs │ │ ├── ListSeparator.cs │ │ ├── LogicalExpression.cs │ │ ├── LogicalOperatorKind.cs │ │ ├── Parsers.cs │ │ ├── QuotedString.cs │ │ ├── QuotedStringLiteral.cs │ │ ├── SimpleList.cs │ │ ├── SimpleListItem.cs │ │ ├── StringContent.cs │ │ ├── Symbol.cs │ │ └── Tokens.cs │ ├── MSBuildImport.cs │ ├── MSBuildItemGroup.cs │ ├── MSBuildModelExtensions.cs │ ├── MSBuildObject.cs │ ├── MSBuildObjectKind.cs │ ├── MSBuildObjectLocator.cs │ ├── MSBuildProperty.cs │ ├── MSBuildSchemaHelp.cs │ ├── MSBuildSdkImport.cs │ ├── MSBuildTarget.cs │ ├── MSBuildTaskMetadataCache.cs │ ├── MSBuildTaskScanner.Metadata.cs │ ├── MSBuildTaskScanner.cs │ ├── MSBuildUnresolvedImport.cs │ ├── MSBuildUnresolvedSdkImport.cs │ ├── MSBuildUnusedItemGroup.cs │ ├── MSBuildUnusedProperty.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── WellKnownElementPaths.cs │ └── XmlLocationMSBuildExtensions.cs ├── LanguageServer.SemanticModel.Xml │ ├── LanguageServer.SemanticModel.Xml.csproj │ ├── PaddingType.cs │ ├── XSAttribute.cs │ ├── XSElement.cs │ ├── XSElementText.cs │ ├── XSElementWithContent.cs │ ├── XSEmptyElement.cs │ ├── XSInvalidAttribute.cs │ ├── XSInvalidElement.cs │ ├── XSNode.cs │ ├── XSNodeExtensions.cs │ ├── XSNodeKind.cs │ ├── XSParser.cs │ ├── XSPath.cs │ ├── XSPathSegment.cs │ ├── XSPathSegmentExtensions.cs │ ├── XSWhitespace.cs │ ├── XmlExceptionExtensions.cs │ ├── XmlLocation.cs │ ├── XmlLocationExtensions.cs │ ├── XmlLocationFlags.cs │ ├── XmlLocator.cs │ ├── XmlModelConversions.cs │ └── XmlSyntaxHelper.cs └── LanguageServer │ ├── LanguageServer.csproj │ ├── LoggingModule.cs │ ├── Program.cs │ └── Terminator.cs └── test └── LanguageServer.Engine.Tests ├── App.config ├── AssertParser.cs ├── ExpressionTests ├── ComparisonParserTests.cs ├── EvaluationParserTests.cs ├── ExpressionListParserTests.cs ├── ExpressionParserTests.cs ├── FunctionCallParserTests.cs ├── GroupedExpressionTests.cs ├── ItemGroupParserTests.cs ├── ItemGroupTransformParserTests.cs ├── ItemMetatadaParserTests.cs ├── LocatorExpressionTests.cs ├── LogicalParserTests.cs ├── QuotedStringLiteralParserTests.cs ├── QuotedStringParserTests.cs ├── RootParserTests.cs ├── SimpleListParserTests.cs ├── TokenParserTests.cs └── TypeRefParserTests.cs ├── LanguageServer.Engine.Tests.csproj ├── Logging ├── SerilogTestOutputExtensions.cs └── TestOutputSink.cs ├── MSBuildEngineFixture.cs ├── MSBuildObjectLocatorTests.cs ├── MSBuildTests.cs ├── PositionTests.cs ├── RangeTests.cs ├── Stubs ├── StubDiagnosticPublisher.cs └── StubLanguageServer.cs ├── TaskScannerTests.cs ├── TestBase.cs ├── TestFiles ├── Invalid1.DoubleOpeningTag.xml ├── Invalid1.EmptyOpeningTag.xml ├── Invalid2.DoubleOpeningTag.xml ├── Invalid2.EmptyOpeningTag.ChildOfRoot.xml ├── Invalid2.EmptyOpeningTag.xml ├── Invalid2.NoClosingTag.xml ├── Test1.xml ├── Test2.xml ├── Test3.xml └── Test4.xml ├── TestHelper.cs ├── TestProject.cs ├── TestProjects ├── Project1.csproj └── RedefineProperty.SameFile.csproj ├── TextPositionsTests.cs ├── XSParserTests.cs ├── XSPathTests.cs └── XmlLocatorTests.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | 11 | [*.{yml,xml,csproj,props,targets}] 12 | indent_size = 2 13 | 14 | [*.cs] 15 | csharp_style_throw_expression = false:none 16 | csharp_indent_case_contents = true 17 | csharp_indent_case_contents_when_block = false 18 | 19 | csharp_style_var_for_built_in_types = true 20 | csharp_style_var_when_type_is_apparent = true:warning 21 | csharp_style_var_elsewhere = false 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run language server host", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceRoot}/src/LanguageServer/bin/Debug/net6.0/MSBuildProjectTools.LanguageServer.Host.dll", 13 | "args": [], 14 | "cwd": "${workspaceRoot}", 15 | "stopAtEntry": true, 16 | "env": { 17 | "MSBUILD_PROJECT_TOOLS_VERBOSE_LOGGING": "1", 18 | "MSBUILD_PROJECT_TOOLS_LOG_FILE": "D:\\Stage\\msbpt.log" 19 | }, 20 | "console": "internalConsole" 21 | }, 22 | { 23 | "name": "Run tests", 24 | "type": "coreclr", 25 | "request": "launch", 26 | "preLaunchTask": "build", 27 | // If you have changed target frameworks, make sure to update the program path. 28 | "program": "${workspaceFolder}/test/LanguageServer.Engine.Tests/bin/Debug/net6.0/MSBuildProjectTools.LanguageServer.Engine.Tests.dll", 29 | "args": [], 30 | "cwd": "${workspaceFolder}/test/LanguageServer.Engine.Tests", 31 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 32 | "console": "internalConsole", 33 | "stopAtEntry": false, 34 | "internalConsoleOptions": "openOnSessionStart" 35 | }, 36 | { 37 | "name": ".NET Core Attach", 38 | "type": "coreclr", 39 | "request": "attach", 40 | "processId": "${command:pickProcess}" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/MSBuildProjectTools.sln" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 5 | 6 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDeployment 2 | next-version: 0.6.3 3 | branches: 4 | main: 5 | regex: ^master$|^main$ 6 | mode: ContinuousDelivery 7 | source-branches: [] 8 | increment: Patch 9 | prevent-increment-of-merged-branch-version: true 10 | is-mainline: true 11 | feature: 12 | regex: ^feature(s)?\/[\d-]+ 13 | mode: ContinuousDelivery 14 | source-branches: 15 | - main 16 | - bugfix 17 | tag: useBranchName 18 | increment: Patch 19 | prevent-increment-of-merged-branch-version: false 20 | track-merge-target: false 21 | tracks-release-branches: false 22 | is-release-branch: false 23 | is-mainline: false 24 | bugfix: 25 | regex: ^bug(s)?\/[\d-]+|^hotfix(s)?\/[\d-]+|^fix(s)?\/[\d-]+ 26 | mode: ContinuousDeployment 27 | source-branches: 28 | - main 29 | tag: useBranchName 30 | increment: Patch 31 | prevent-increment-of-merged-branch-version: false 32 | track-merge-target: false 33 | tracks-release-branches: false 34 | is-release-branch: false 35 | is-mainline: false 36 | ignore: 37 | sha: 38 | - 937c06026995c95da4bb13a02714a81db87876b1 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adam Friedman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MSBuildProjectTools.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /OSSREADME.json: -------------------------------------------------------------------------------- 1 | // ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS: 2 | 3 | [{ 4 | "name": "Microsoft/msbuild", 5 | "version": "0.0.0", 6 | "license": "MIT", 7 | "repositoryURL": "https://github.com/Microsoft/msbuild", 8 | "description": "Documentation / help content for well-known MSBuild elements, attributes, properties, and item types was derived from https://github.com/Microsoft/msbuild/blob/master/src/MSBuild/Microsoft.Build.CommonTypes.xsd, as well as https://github.com/MicrosoftDocs/visualstudio-docs/tree/master/docs/msbuild." 9 | }] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSBuild project file tools 2 | 3 | An [LSP](https://github.com/Microsoft/language-server-protocol)-compatible language service that provides intellisense for MSBuild project files, including auto-complete for `` elements. 4 | 5 | For more information, see [msbuild-project-tools-vscode](https://github.com/tintoy/msbuild-project-tools-vscode). 6 | 7 | ## Building from source 8 | 9 | See [BUILDING.md](docs/BUILDING.md). 10 | 11 | ## Design 12 | 13 | See [architectural overview](docs/architecture/overview.md) for details (this is a work-in-progress; if you have questions, feel free to create an issue). 14 | 15 | ## Limitations 16 | 17 | * Limited intellisense is available for dynamic `PropertyGroup` / `ItemGroup` declarations (i.e. those appearing inside a `Target` element); these are only evaluated when the project is built and so very little information about them is available to us when statically evaluating the project (see [tintoy/msbuild-project-tools-server#5](https://github.com/tintoy/msbuild-project-tools-server/issues/5#issuecomment-383352512) for details). 18 | * Support for task completions is experimental; if you find a problem with it, please [create an issue](https://github.com/tintoy/msbuild-project-tools-server/issues/new). 19 | * Support for MSBuild expressions is experimental; if you find a problem with it, please [create an issue](https://github.com/tintoy/msbuild-project-tools-server/issues/new). 20 | * If you open more than one project at a time (or navigate to imported projects), subsequent projects will be loaded into the same MSBuild project collection as the first project. Once you have closed the last project file, the next project file you open will become the master project. The master project will become selectable in a later release. 21 | 22 | ## Questions / bug reports 23 | 24 | If you have questions, feedback, feature requests, or would like to report a bug, please feel free to reach out by creating an issue. When reporting a bug, please try to include as much information as possible about what you were doing at the time, what you expected to happen, and what actually happened. 25 | 26 | If you're interested in collaborating that'd be great, too :-) 27 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | 6 | tags: 7 | include: 8 | - 'v*' 9 | 10 | pr: 11 | branches: 12 | include: 13 | - master 14 | 15 | pool: 16 | vmImage: ubuntu-latest 17 | 18 | variables: 19 | buildConfiguration: 'Release' 20 | 21 | steps: 22 | - task: gitversion/setup@0 23 | displayName: 'Install GitVersion' 24 | 25 | inputs: 26 | versionSpec: 5.x 27 | 28 | - task: GitVersion/execute@0 29 | displayName: 'Determine build version' 30 | name: GitVersion 31 | 32 | - task: DotNetCoreCLI@2 33 | displayName: 'Restore packages' 34 | 35 | inputs: 36 | command: 'restore' 37 | projects: './MSBuildProjectTools.sln' 38 | restoreArguments: '/p:VersionPrefix="$(GitVersion.MajorMinorPatch)" /p:VersionSuffix="$(GitVersion.PreReleaseTag)"' 39 | 40 | - task: DotNetCoreCLI@2 41 | displayName: 'Build solution' 42 | 43 | inputs: 44 | command: 'build' 45 | projects: './MSBuildProjectTools.sln' 46 | arguments: '--configuration "$(buildConfiguration)" /p:VersionPrefix="$(GitVersion.MajorMinorPatch)" /p:VersionSuffix="$(GitVersion.PreReleaseTag)"' 47 | 48 | - task: DotNetCoreCLI@2 49 | displayName: 'Run tests' 50 | 51 | inputs: 52 | command: 'test' 53 | projects: './test/LanguageServer.Engine.Tests/LanguageServer.Engine.Tests.csproj' 54 | arguments: '--configuration "$(buildConfiguration)" /p:VersionPrefix="$(GitVersion.MajorMinorPatch)" /p:VersionSuffix="$(GitVersion.PreReleaseTag)"' 55 | 56 | - task: DotNetCoreCLI@2 57 | displayName: 'Publish language server' 58 | 59 | inputs: 60 | command: 'publish' 61 | publishWebProjects: false 62 | projects: './src/LanguageServer/LanguageServer.csproj' 63 | arguments: '--configuration "$(buildConfiguration)" -o "./out/language-server" /p:VersionPrefix="$(GitVersion.MajorMinorPatch)" /p:VersionSuffix="$(GitVersion.PreReleaseTag)"' 64 | modifyOutputPath: false 65 | zipAfterPublish: false 66 | 67 | - task: ArchiveFiles@2 68 | displayName: 'Package language-server artifact' 69 | 70 | inputs: 71 | rootFolderOrFile: './out/language-server' 72 | includeRootFolder: false 73 | archiveType: 'zip' 74 | archiveFile: '$(Build.ArtifactStagingDirectory)/language-server.zip' 75 | replaceExistingArchive: true 76 | 77 | - task: PublishBuildArtifacts@1 78 | displayName: 'Publish language-server artifact' 79 | 80 | inputs: 81 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/language-server.zip' 82 | ArtifactName: 'language-server' 83 | publishLocation: 'Container' 84 | 85 | - task: GitHubRelease@1 86 | displayName: 'Create GitHub release from tag' 87 | 88 | condition: contains(variables['Build.SourceBranch'], 'refs/tags/v') 89 | 90 | inputs: 91 | gitHubConnection: 'github.com_tintoy' 92 | repositoryName: '$(Build.Repository.Name)' 93 | action: 'create' 94 | target: '$(Build.SourceVersion)' 95 | tagSource: 'gitTag' 96 | tagPattern: '^v\d+\.\d+.\d+(-[A-Za-z0-9%\.]+)?$' 97 | addChangeLog: true 98 | assets: '$(Build.ArtifactStagingDirectory)/*.zip' 99 | assetUploadMode: replace 100 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | $dotnet = Get-Command 'dotnet' 2 | 3 | & $dotnet restore 4 | & $dotnet publish "$PSScriptRoot\src\LanguageServer\LanguageServer.csproj" -o "$PSScriptRoot\out\language-server" 5 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'Restoring Nuget packages...' 4 | dotnet restore 5 | 6 | echo 'Building language server...' 7 | dotnet publish src/LanguageServer/LanguageServer.csproj -o $PWD/out/language-server 8 | 9 | echo 'Done.' 10 | -------------------------------------------------------------------------------- /dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./build.sh 4 | 5 | ZIPFILE="$PWD/out/msbuild-project-tools.zip" 6 | 7 | if [ -f $ZIPFILE ]; then 8 | rm $ZIPFILE 9 | fi 10 | 11 | echo "Creating $ZIPFILE..." 12 | 13 | zip -qr $ZIPFILE out/ 14 | 15 | echo "Done." 16 | -------------------------------------------------------------------------------- /docs/BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building MSBuild Project Tools language server 2 | 3 | You'll need: 4 | 5 | 1. .NET 6.0.0 or newer 6 | 7 | To build: 8 | 9 | 1. `powershell build.ps1` 10 | 11 | Debugging: 12 | 13 | There are 2 main ways of debugging this LSP: 14 | - Debugging tests: Just debug a test via your IDE, nothing special here 15 | - Debugging as part of VS Code extension: 16 | 1. Clone [extension repo](https://github.com/tintoy/msbuild-project-tools-vscode) for this LSP 17 | 2. Follow [the guide](https://github.com/tintoy/msbuild-project-tools-vscode/blob/master/docs/BUILDING.md) and start debugging that extension 18 | 3. After new VS Code window appeared, choose `Attach to LSP process` debug configuration and manually attach to LSP process, spawned by extension 19 | 4. Now you can trigger various LSP calls via VS Code window (the one that appeared after you started debugging extension) and hit your breakpoints in LSP code 20 | 21 | Note that in the second setup you don't work with this repo directly, but through git submodules system in extension repo -------------------------------------------------------------------------------- /docs/architecture/diagrams.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/msbuild-project-tools-server/5f5613f00779b6d4e0f48aacd82d3ebcc4b040c6/docs/architecture/diagrams.pptx -------------------------------------------------------------------------------- /docs/architecture/images/lsp-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/msbuild-project-tools-server/5f5613f00779b6d4e0f48aacd82d3ebcc4b040c6/docs/architecture/images/lsp-layers.png -------------------------------------------------------------------------------- /docs/architecture/images/server-layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/msbuild-project-tools-server/5f5613f00779b6d4e0f48aacd82d3ebcc4b040c6/docs/architecture/images/server-layers.png -------------------------------------------------------------------------------- /docs/architecture/overview.md: -------------------------------------------------------------------------------- 1 | # Architectural overview 2 | 3 | Most of the functionality provided by MSBuild project tools is implemented by an LSP-compliant language service. 4 | 5 | _Note: this documentation is a work-in-progress; if you have questions, feel free to create an issue._ 6 | 7 | # Language Server Protocol 8 | 9 | The extension for VS Code extension communicates with this service using [vscode-languageclient](https://www.npmjs.com/package/vscode-languageclient) over STDIN/STDOUT. 10 | 11 | ![LSP - layers](images/lsp-layers.png) 12 | 13 | # Server 14 | 15 | The server is implemented in several layers. 16 | 17 | * **Protocol** 18 | * We use OmniSharp's LSP and JSON-RPC implementation. 19 | * Everything is tied together by the [LanguageServer](https://github.com/OmniSharp/csharp-language-server-protocol/blob/4bc2a15e43593e6e459e9b0784cb85fadcf89c34/src/Lsp/LanguageServer.cs#L22). 20 | * **Server** 21 | * We use Autofac for dependency-injection and Serilog for logging (including a custom logger that forwards log entries to the LSP client, e.g. VS Code, for diplay). 22 | * A [Workspace](../../src/LanguageServer.Engine/Documents/Workspace.cs) handles all projects for a given base directory. 23 | * Most of the state for each open MSBuild project is held by a [ProjectDocument](../../src/LanguageServer.Engine/Documents/ProjectDocument.cs) (see [MasterProjectDocument](../../src/LanguageServer.Engine/Documents/MasterProjectDocument.cs) and [SubProjectDocument](../../src/LanguageServer.Engine/Documents/SubProjectDocument.cs) for further details). 24 | * Document state is protected by an `AsyncReaderWriterLock` (it is the caller's responsibility to call `XXXLock` / `XXXLockAsync` as required). 25 | * **Handlers** 26 | * The [DocumentSyncHandler](../../src/LanguageServer.Engine/Handlers/DocumentSyncHandler.cs) handles synchronisation of document state with the client. We expect a `textDocument/didOpen` notification as the trigger to open and load a project. 27 | * The [CompletionHandler](../../src/LanguageServer.Engine/Handlers/CompletionHandler.cs) calls multiple [completion providers](../../src/LanguageServer.Engine/CompletionProviders) in parallel. 28 | * If all completion providers return `null`, then the `CompletionHandler` will return null to the caller (indicating no completions are available). 29 | * If only _some_ completion providers return `null`, then the `CompletionHandler` will ignore them and return non-`null` completions. 30 | * If any provider indicates that their completion list is incomplete, then the composite completion list will be marked as incomplete. 31 | 32 | ![server layers](images/server-layers.png) 33 | 34 | # Syntax and semantic models 35 | 36 | * At the lowest level, we have `Microsoft.Language.Xml` and `Microsoft.Build.Construction`. 37 | * Above that we have `MSBuildProjectTools.SemanticModel.Xml`, `MSBuildProjectTools.SemanticModel.MSBuild`, and `Microsoft.Build.Evaluation`. 38 | 39 | The semantic models build on the syntax models to create a more high-level API for evaluating project contents. 40 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100", 4 | "rollForward": "feature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | true 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Help/ElementHelp.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.Help 2 | { 3 | /// 4 | /// Help information for an MSBuild element. 5 | /// 6 | public class ElementHelp 7 | { 8 | /// 9 | /// The property description. 10 | /// 11 | public string Description { get; init; } 12 | 13 | /// 14 | /// Help link for the element (if any). 15 | /// 16 | public string HelpLink { get; init; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Help/ItemHelp.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Help 4 | { 5 | /// 6 | /// Help information for an MSBuild item. 7 | /// 8 | public class ItemHelp 9 | { 10 | /// 11 | /// A description of the item. 12 | /// 13 | public string Description { get; init; } 14 | 15 | /// 16 | /// A link to the item type's documentation (if available). 17 | /// 18 | public string HelpLink { get; init; } 19 | 20 | /// 21 | /// Descriptions for the item's metadata. 22 | /// 23 | public SortedDictionary Metadata { get; init; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Help/PropertyHelp.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Help 4 | { 5 | /// 6 | /// Help information for an MSBuild property. 7 | /// 8 | public class PropertyHelp 9 | { 10 | /// 11 | /// The property description. 12 | /// 13 | public string Description { get; init; } 14 | 15 | /// 16 | /// A link to the property's documentation (if available). 17 | /// 18 | public string HelpLink { get; init; } 19 | 20 | /// 21 | /// The property's default values (if specified, the completion's snippet will present a drop-down list of values for the user to choose from as the property value.'). 22 | /// 23 | public List DefaultValues { get; init; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Help/TaskHelp.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.Help 5 | { 6 | /// 7 | /// Help information for an MSBuild task. 8 | /// 9 | public class TaskHelp 10 | { 11 | /// 12 | /// A description of the task. 13 | /// 14 | public string Description { get; init; } 15 | 16 | /// 17 | /// A link to the task's documentation (if available). 18 | /// 19 | public string HelpLink { get; init; } 20 | 21 | /// 22 | /// The task's parameters. 23 | /// 24 | public SortedDictionary Parameters { get; init; } 25 | } 26 | 27 | /// 28 | /// Help information for an MSBuild task parameter. 29 | /// 30 | public class TaskParameterHelp 31 | { 32 | /// 33 | /// A description of the task parameter. 34 | /// 35 | public string Description { get; init; } 36 | 37 | /// 38 | /// A link to the task parameter's documentation (if available). 39 | /// 40 | public string HelpLink { get; init; } 41 | 42 | /// 43 | /// A description of the task parameter data-type. 44 | /// 45 | [JsonPropertyName("type")] 46 | public string TypeDescription { get; init; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/LanguageServer.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | MSBuildProjectTools.LanguageServer.Common 4 | MSBuildProjectTools.LanguageServer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Utilities/LinqHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.Utilities 6 | { 7 | /// 8 | /// Helper methods for working with LINQ. 9 | /// 10 | public static class LinqHelper 11 | { 12 | /// 13 | /// Flatten the sequence, enumerating nested sequences. 14 | /// 15 | /// 16 | /// The source element type. 17 | /// 18 | /// 19 | /// The source sequence of sequences. 20 | /// 21 | /// 22 | /// The flattened sequence. 23 | /// 24 | public static IEnumerable Flatten(this IEnumerable> source) 25 | { 26 | if (source == null) 27 | throw new ArgumentNullException(nameof(source)); 28 | 29 | return source.SelectMany(items => items); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Utilities/NuGetResourceProviderVersions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Utilities 4 | { 5 | /// 6 | /// Well-known versions of the NuGet resource providers. 7 | /// 8 | [Flags] 9 | public enum NuGetResourceProviderVersions 10 | { 11 | /// 12 | /// No resource providers. 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// Version 2 of the NuGet providers. 18 | /// 19 | /// 20 | /// No longer supported by the version of the NuGet client libraries in use by MSBuild Project Tools. 21 | /// 22 | V2 = 1, 23 | 24 | /// 25 | /// Version 3 of the NuGet providers. 26 | /// 27 | V3 = 2, 28 | 29 | /// 30 | /// The currently supported version(s) of the NuGet providers. 31 | /// 32 | Current = V3 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/LanguageServer.Common/Utilities/VSCodeDocumentUri.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.Utilities 5 | { 6 | /// 7 | /// Helper methods for working with VSCode document s. 8 | /// 9 | public static class VSCodeDocumentUri 10 | { 11 | /// 12 | /// Get the local file-system path for the specified document URI. 13 | /// 14 | /// 15 | /// The document URI. 16 | /// 17 | /// 18 | /// The file-system path, or null if the URI does not represent a file-system path. 19 | /// 20 | public static string GetFileSystemPath(Uri documentUri) 21 | { 22 | if (documentUri == null) 23 | throw new ArgumentNullException(nameof(documentUri)); 24 | 25 | if (documentUri.Scheme != Uri.UriSchemeFile) 26 | return null; 27 | 28 | // The language server protocol represents "C:\Foo\Bar" as "file:///c:/foo/bar". 29 | string fileSystemPath = Uri.UnescapeDataString(documentUri.AbsolutePath); 30 | if (Path.DirectorySeparatorChar == '\\') 31 | { 32 | if (fileSystemPath.StartsWith("/")) 33 | fileSystemPath = fileSystemPath[1..]; 34 | 35 | fileSystemPath = fileSystemPath.Replace('/', '\\'); 36 | } 37 | 38 | return fileSystemPath; 39 | } 40 | 41 | /// 42 | /// Convert a file-system path to a VSCode document URI. 43 | /// 44 | /// 45 | /// The file-system path. 46 | /// 47 | /// 48 | /// The VSCode document URI. 49 | /// 50 | public static Uri FromFileSystemPath(string fileSystemPath) 51 | { 52 | if (string.IsNullOrWhiteSpace(fileSystemPath)) 53 | throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'fileSystemPath'.", nameof(fileSystemPath)); 54 | 55 | if (!Path.IsPathRooted(fileSystemPath)) 56 | throw new ArgumentException($"Path '{fileSystemPath}' is not an absolute path.", nameof(fileSystemPath)); 57 | 58 | if (Path.DirectorySeparatorChar == '\\') 59 | return new Uri("file:///" + fileSystemPath.Replace('\\', '/')); 60 | 61 | return new Uri("file://" + fileSystemPath); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CompletionProviders/ICompletionProvider.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.CompletionProviders 5 | { 6 | using Documents; 7 | using SemanticModel; 8 | using System.Threading; 9 | 10 | /// 11 | /// Represents a source for completions. 12 | /// 13 | public interface ICompletionProvider 14 | { 15 | /// 16 | /// Provide completions for the specified location. 17 | /// 18 | /// 19 | /// The where completions are requested. 20 | /// 21 | /// 22 | /// The that contains the . 23 | /// 24 | /// 25 | /// The character(s), if any, that triggered completion. 26 | /// 27 | /// 28 | /// A that can be used to cancel the operation. 29 | /// 30 | /// 31 | /// A that resolves either a list of s, or null if no completions are provided. 32 | /// 33 | Task ProvideCompletionsAsync(XmlLocation location, ProjectDocument projectDocument, string triggerCharacters, CancellationToken cancellationToken); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CompletionProviders/PropertyConditionCompletionProvider.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 2 | using Serilog; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using LspModels = OmniSharp.Extensions.LanguageServer.Protocol.Models; 9 | 10 | namespace MSBuildProjectTools.LanguageServer.CompletionProviders 11 | { 12 | using Documents; 13 | using SemanticModel; 14 | using Utilities; 15 | 16 | /// 17 | /// Completion provider the Condition attribute of a property element. 18 | /// 19 | public class PropertyConditionCompletionProvider 20 | : CompletionProvider 21 | { 22 | /// 23 | /// Create a new . 24 | /// 25 | /// 26 | /// The application logger. 27 | /// 28 | public PropertyConditionCompletionProvider(ILogger logger) 29 | : base(logger) 30 | { 31 | } 32 | 33 | /// 34 | /// Provide completions for the specified location. 35 | /// 36 | /// 37 | /// The where completions are requested. 38 | /// 39 | /// 40 | /// The that contains the . 41 | /// 42 | /// 43 | /// The character(s), if any, that triggered completion. 44 | /// 45 | /// 46 | /// A that can be used to cancel the operation. 47 | /// 48 | /// 49 | /// A that resolves either a s, or null if no completions are provided. 50 | /// 51 | public override async Task ProvideCompletionsAsync(XmlLocation location, ProjectDocument projectDocument, string triggerCharacters, CancellationToken cancellationToken) 52 | { 53 | if (location == null) 54 | throw new ArgumentNullException(nameof(location)); 55 | 56 | if (projectDocument == null) 57 | throw new ArgumentNullException(nameof(projectDocument)); 58 | 59 | var completions = new List(); 60 | 61 | using (await projectDocument.Lock.ReaderLockAsync(cancellationToken)) 62 | { 63 | if (!location.IsAttributeValue(out XSAttribute conditionAttribute) || conditionAttribute.Name != "Condition") 64 | return null; 65 | 66 | if (conditionAttribute.Element.ParentElement?.Name != "PropertyGroup") 67 | return null; 68 | 69 | LspModels.Range replaceRange = conditionAttribute.ValueRange.ToLsp(); 70 | 71 | completions.Add(new CompletionItem 72 | { 73 | Label = "If not already defined", 74 | Detail = "Condition", 75 | Documentation = "Only use this property if the property does not already have a value.", 76 | TextEdit = new TextEdit 77 | { 78 | NewText = $"'$({conditionAttribute.Element.Name})' == ''", 79 | Range = replaceRange 80 | } 81 | }); 82 | } 83 | 84 | if (completions.Count == 0) 85 | return null; 86 | 87 | return new CompletionList(completions, isIncomplete: false); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CompletionProviders/TaskCompletionProvider.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.CompletionProviders 7 | { 8 | using Documents; 9 | using SemanticModel; 10 | 11 | /// 12 | /// Base class for MSBuild task completion providers. 13 | /// 14 | public abstract class TaskCompletionProvider 15 | : CompletionProvider 16 | { 17 | /// 18 | /// Create a new . 19 | /// 20 | /// 21 | /// The application logger. 22 | /// 23 | protected TaskCompletionProvider(ILogger logger) 24 | : base(logger) 25 | { 26 | } 27 | 28 | /// 29 | /// Get all tasks defined in the project. 30 | /// 31 | /// 32 | /// The project document. 33 | /// 34 | /// 35 | /// A dictionary of task metadata, keyed by task name. 36 | /// 37 | protected static Dictionary GetProjectTasks(ProjectDocument projectDocument) 38 | { 39 | if (projectDocument == null) 40 | throw new ArgumentNullException(nameof(projectDocument)); 41 | 42 | // We trust that all tasks discovered via GetMSBuildProjectTaskAssemblies are accessible in the current project. 43 | 44 | var tasks = new Dictionary(); 45 | foreach (MSBuildTaskAssemblyMetadata assemblyMetadata in projectDocument.GetMSBuildProjectTaskAssemblies()) 46 | { 47 | foreach (MSBuildTaskMetadata task in assemblyMetadata.Tasks) 48 | tasks[task.Name] = task; 49 | } 50 | 51 | return tasks; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CustomProtocol/BusyNotification.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.CustomProtocol 5 | { 6 | /// 7 | /// Parameters for notifying the LSP language client that the language service is (or is not) busy. 8 | /// 9 | [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] 10 | public class BusyNotificationParams 11 | { 12 | /// 13 | /// Create new . 14 | /// 15 | public BusyNotificationParams() 16 | { 17 | } 18 | 19 | /// 20 | /// Is the language service busy? 21 | /// 22 | public bool IsBusy { get; set; } 23 | 24 | /// 25 | /// If the language service is busy, a message describing why. 26 | /// 27 | public string Message { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CustomProtocol/CustomCompletionHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using OmniSharp.Extensions.LanguageServer.Protocol; 3 | using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; 4 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.CustomProtocol 7 | { 8 | /// 9 | /// Custom handler for "textDocument/completion" that accepts (the built-in version simply uses , which doesn't include information about how completion was triggered). 10 | /// 11 | [Method("textDocument/completion")] 12 | public interface ICustomCompletionHandler 13 | : IRequestHandler, IJsonRpcHandler, IRegistration, ICapability 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/CustomProtocol/DidChangeConfigurationSettings.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using OmniSharp.Extensions.LanguageServer.Protocol; 3 | using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace MSBuildProjectTools.LanguageServer.CustomProtocol 8 | { 9 | /// 10 | /// Custom handler for "workspace/didChangeConfiguration" with the configuration as a . 11 | /// 12 | [Method("workspace/didChangeConfiguration")] 13 | public interface IDidChangeConfigurationSettingsHandler 14 | : INotificationHandler, IJsonRpcHandler, IRegistration, ICapability 15 | { 16 | } 17 | 18 | /// 19 | /// Notification parameters for "workspace/didChangeConfiguration". 20 | /// 21 | public class DidChangeConfigurationObjectParams 22 | { 23 | /// 24 | /// The current settings. 25 | /// 26 | [JsonProperty("settings")] 27 | public JToken Settings; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Diagnostics/IPublishDiagnostics.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.Diagnostics 6 | { 7 | /// 8 | /// Represents a facility for publishing diagnostics (i.e. warnings, errors, etc). 9 | /// 10 | public interface IPublishDiagnostics 11 | { 12 | /// 13 | /// Publish the specified diagnostics. 14 | /// 15 | /// 16 | /// The URI of the document that the diagnostics apply to. 17 | /// 18 | /// 19 | /// A sequence of s to publish. 20 | /// 21 | void Publish(Uri documentUri, IEnumerable diagnostics); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Diagnostics/LspDiagnosticsPublisher.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol; 2 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 3 | using OmniSharp.Extensions.LanguageServer.Server; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace MSBuildProjectTools.LanguageServer.Diagnostics 9 | { 10 | /// 11 | /// An implementation of that publishes diagnostics via LSP. 12 | /// 13 | class LspDiagnosticsPublisher 14 | : IPublishDiagnostics 15 | { 16 | /// 17 | /// The LSP . 18 | /// 19 | readonly ILanguageServer _languageServer; 20 | 21 | /// 22 | /// Create a new . 23 | /// 24 | /// 25 | /// The LSP . 26 | /// 27 | public LspDiagnosticsPublisher(ILanguageServer languageServer) 28 | { 29 | if (languageServer == null) 30 | throw new ArgumentNullException(nameof(languageServer)); 31 | 32 | _languageServer = languageServer; 33 | } 34 | 35 | /// 36 | /// Publish the specified diagnostics. 37 | /// 38 | /// 39 | /// The URI of the document that the diagnostics apply to. 40 | /// 41 | /// 42 | /// A sequence of s to publish. 43 | /// 44 | public void Publish(Uri documentUri, IEnumerable diagnostics) 45 | { 46 | if (documentUri == null) 47 | throw new ArgumentNullException(nameof(documentUri)); 48 | 49 | diagnostics ??= Enumerable.Empty(); 50 | 51 | _languageServer.PublishDiagnostics(new PublishDiagnosticsParams 52 | { 53 | Uri = documentUri, 54 | Diagnostics = diagnostics.ToArray() 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Documents/ProjectDocumentKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.Documents 2 | { 3 | /// 4 | /// A kind of . 5 | /// 6 | public enum ProjectDocumentKind 7 | { 8 | /// 9 | /// A project (*.*proj). 10 | /// 11 | Project = 1, 12 | 13 | /// 14 | /// A properties file (*.props). 15 | /// 16 | Properties = 2, 17 | 18 | /// 19 | /// A targets file (*.targets). 20 | /// 21 | Targets = 3, 22 | 23 | /// 24 | /// Some other file type (*.*). 25 | /// 26 | Other = 4 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Handlers/Handler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using Serilog; 3 | using System; 4 | using System.Reactive.Disposables; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.Handlers 7 | { 8 | using OmniSharp.Extensions.LanguageServer.Server; 9 | using Utilities; 10 | 11 | /// 12 | /// The base class for language server event handlers. 13 | /// 14 | public abstract class Handler 15 | : IJsonRpcHandler 16 | { 17 | /// 18 | /// Create a new . 19 | /// 20 | /// 21 | /// The language server. 22 | /// 23 | /// 24 | /// The application logger. 25 | /// 26 | protected Handler(ILanguageServer server, ILogger logger) 27 | { 28 | if (server == null) 29 | throw new ArgumentNullException(nameof(server)); 30 | 31 | Server = server; 32 | Log = logger.ForContext(GetType()); 33 | } 34 | 35 | /// 36 | /// The handler's logger. 37 | /// 38 | protected ILogger Log { get; } 39 | 40 | /// 41 | /// The language server. 42 | /// 43 | protected ILanguageServer Server { get; } 44 | 45 | /// 46 | /// Add an activity / log-context scope for an operation. 47 | /// 48 | /// 49 | /// The operation name. 50 | /// 51 | /// 52 | /// An representing the log-context scope. 53 | /// 54 | protected static IDisposable BeginOperation(string operationName) 55 | { 56 | if (string.IsNullOrWhiteSpace(operationName)) 57 | throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'operationName'.", nameof(operationName)); 58 | 59 | return new CompositeDisposable( 60 | ActivityCorrelationManager.BeginActivityScope(), 61 | Serilog.Context.LogContext.PushProperty("Operation", operationName) 62 | ); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/LanguageServer.Engine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | MSBuildProjectTools.LanguageServer.Engine 4 | MSBuildProjectTools.LanguageServer 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 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Logging/ActivityIdEnricher.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | using Serilog.Events; 3 | using System; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.Logging 6 | { 7 | using Utilities; 8 | 9 | /// 10 | /// Serilog event enricher that adds the current logical activity Id. 11 | /// 12 | class ActivityIdEnricher 13 | : ILogEventEnricher 14 | { 15 | /// 16 | /// Enrich the specified with the current activity Id (if any). 17 | /// 18 | /// 19 | /// The to enrich. 20 | /// 21 | /// 22 | /// The factory for log event properties. 23 | /// 24 | public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) 25 | { 26 | if (logEvent == null) 27 | throw new ArgumentNullException(nameof(logEvent)); 28 | 29 | if (propertyFactory == null) 30 | throw new ArgumentNullException(nameof(propertyFactory)); 31 | 32 | Guid? activityId = ActivityCorrelationManager.CurrentActivityId; 33 | LogEventProperty activityIdProperty = propertyFactory.CreateProperty("ActivityId", activityId); 34 | logEvent.AddPropertyIfAbsent(activityIdProperty); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Logging/SerilogLanguageServerExtensions.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Server; 2 | using Serilog; 3 | using Serilog.Configuration; 4 | using Serilog.Core; 5 | using System; 6 | 7 | namespace MSBuildProjectTools.LanguageServer.Logging 8 | { 9 | /// 10 | /// Extension methods for configuring Serilog. 11 | /// 12 | public static class SerilogLanguageServerExtensions 13 | { 14 | /// 15 | /// Write log events to the language server logging facility. 16 | /// 17 | /// 18 | /// The logger sink configuration. 19 | /// 20 | /// 21 | /// The language server to which events will be logged. 22 | /// 23 | /// 24 | /// The that controls logging. 25 | /// 26 | /// 27 | /// The logger configuration. 28 | /// 29 | public static LoggerConfiguration LanguageServer(this LoggerSinkConfiguration loggerSinkConfiguration, ILanguageServer languageServer, LoggingLevelSwitch levelSwitch) 30 | { 31 | if (loggerSinkConfiguration == null) 32 | throw new ArgumentNullException(nameof(loggerSinkConfiguration)); 33 | 34 | if (languageServer == null) 35 | throw new ArgumentNullException(nameof(languageServer)); 36 | 37 | if (levelSwitch == null) 38 | throw new ArgumentNullException(nameof(levelSwitch)); 39 | 40 | return loggerSinkConfiguration.Sink( 41 | new LanguageServerLoggingSink(languageServer, levelSwitch) 42 | ); 43 | } 44 | 45 | /// 46 | /// Enrich log events with the current logical activity Id (if any). 47 | /// 48 | /// 49 | /// The logger enrichment configuration. 50 | /// 51 | /// 52 | /// The logger configuration. 53 | /// 54 | public static LoggerConfiguration WithCurrentActivityId(this LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) 55 | { 56 | if (loggerEnrichmentConfiguration == null) 57 | throw new ArgumentNullException(nameof(loggerEnrichmentConfiguration)); 58 | 59 | return loggerEnrichmentConfiguration.With(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("LanguageServer.Engine.Tests")] 4 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Utilities/ModelConversions.cs: -------------------------------------------------------------------------------- 1 | using LspModels = OmniSharp.Extensions.LanguageServer.Protocol.Models; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Utilities 4 | { 5 | /// 6 | /// Extension methods for converting models between native and third-party representations. 7 | /// 8 | public static class ModelConversions 9 | { 10 | /// 11 | /// Convert the to its Language Server Protocol equivalent. 12 | /// 13 | /// 14 | /// The to convert. 15 | /// 16 | /// 17 | /// The equivalent . 18 | /// 19 | public static LspModels.Position ToLsp(this Position position) 20 | { 21 | position = position.ToZeroBased(); // LSP is zero-based. 22 | 23 | return new LspModels.Position( 24 | position.LineNumber, 25 | position.ColumnNumber 26 | ); 27 | } 28 | 29 | /// 30 | /// Convert the Language Server Protocol to its native equivalent. 31 | /// 32 | /// 33 | /// The to convert. 34 | /// 35 | /// 36 | /// The equivalent . 37 | /// 38 | public static Position ToNative(this LspModels.Position position) 39 | { 40 | // LSP is zero-based. 41 | return Position.FromZeroBased( 42 | position.Line, 43 | position.Character 44 | ).ToOneBased(); 45 | } 46 | 47 | /// 48 | /// Convert the to its Language Server Protocol equivalent. 49 | /// 50 | /// 51 | /// The to convert. 52 | /// 53 | /// 54 | /// The equivalent . 55 | /// 56 | public static LspModels.Range ToLsp(this Range range) 57 | { 58 | return new LspModels.Range( 59 | range.Start.ToLsp(), 60 | range.End.ToLsp() 61 | ); 62 | } 63 | 64 | /// 65 | /// Convert the Language Server Protocol to its native equivalent. 66 | /// 67 | /// 68 | /// The to convert. 69 | /// 70 | /// 71 | /// The equivalent . 72 | /// 73 | public static Range ToNative(this LspModels.Range range) 74 | { 75 | return new Range( 76 | range.Start.ToNative(), 77 | range.End.ToNative() 78 | ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/LanguageServer.Engine/Utilities/UsingTaskAssemblyEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.Utilities 6 | { 7 | /// 8 | /// Equality comparer for s that compares . 9 | /// 10 | class UsingTaskAssemblyEqualityComparer 11 | : EqualityComparer 12 | { 13 | /// 14 | /// The singleton instance of the . 15 | /// 16 | public static readonly UsingTaskAssemblyEqualityComparer Instance = new UsingTaskAssemblyEqualityComparer(); 17 | 18 | /// 19 | /// Create a new . 20 | /// 21 | UsingTaskAssemblyEqualityComparer() 22 | { 23 | } 24 | 25 | /// 26 | /// Determine whether 2 s are equal. 27 | /// 28 | /// 29 | /// The first . 30 | /// 31 | /// 32 | /// The second . 33 | /// 34 | /// 35 | /// true, if the s are equal; otherwise, false. 36 | /// 37 | public override bool Equals(ProjectUsingTaskElement usingTask1, ProjectUsingTaskElement usingTask2) => string.Equals(usingTask1?.AssemblyFile, usingTask2?.AssemblyFile, StringComparison.OrdinalIgnoreCase); 38 | 39 | /// 40 | /// Get a hash code to represent the specified . 41 | /// 42 | /// 43 | /// The . 44 | /// 45 | /// 46 | /// The hash code. 47 | /// 48 | public override int GetHashCode(ProjectUsingTaskElement usingTask) => usingTask.AssemblyFile?.GetHashCode() ?? 0; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/LanguageServer.SemanticModel.MSBuild.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | MSBuildProjectTools.LanguageServer.SemanticModel.MSBuild 4 | MSBuildProjectTools.LanguageServer.SemanticModel 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | PreserveNewest 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Exceptions; 2 | using System; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | /// 7 | /// Extension methods for MSBuild-related exceptions. 8 | /// 9 | public static class MSBuildExceptionExtensions 10 | { 11 | /// 12 | /// Get the represented by the . 13 | /// 14 | /// 15 | /// The . 16 | /// 17 | /// 18 | /// The XML locator API (if available). 19 | /// 20 | /// 21 | /// The . 22 | /// 23 | public static Range GetRange(this InvalidProjectFileException invalidProjectFileException, XmlLocator xmlLocator) 24 | { 25 | if (invalidProjectFileException == null) 26 | throw new ArgumentNullException(nameof(invalidProjectFileException)); 27 | 28 | var startPosition = new Position( 29 | invalidProjectFileException.LineNumber, 30 | invalidProjectFileException.ColumnNumber 31 | ); 32 | 33 | // Attempt to use the range of the actual XML that the exception refers to. 34 | XmlLocation location = xmlLocator?.Inspect(startPosition); 35 | if (location != null) 36 | return location.Node.Range; 37 | 38 | // Otherwise, fall back to using the exception's declared end position... 39 | var endPosition = new Position( 40 | invalidProjectFileException.EndLineNumber, 41 | invalidProjectFileException.EndColumnNumber 42 | ); 43 | 44 | // ...although it's sometimes less reliable. 45 | if (endPosition == Position.Zero) 46 | endPosition = startPosition; 47 | 48 | return new Range(startPosition, endPosition); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/Compare.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild comparison expression. 7 | /// 8 | public class Compare 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public Compare() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.Compare; 22 | 23 | /// 24 | /// The kind of comparison represented by the expression. 25 | /// 26 | public ComparisonKind ComparisonKind { get; internal set; } 27 | 28 | /// 29 | /// The left-hand operand. 30 | /// 31 | public ExpressionNode Left => Children[0]; 32 | 33 | /// 34 | /// The right-hand operand. 35 | /// 36 | public ExpressionNode Right => Children[1]; 37 | 38 | /// 39 | /// Get a string representation of the expression node. 40 | /// 41 | /// 42 | /// The string representation. 43 | /// 44 | public override string ToString() => $"MSBuild Compare ({ComparisonKind}) expression @ {Range}"; 45 | 46 | /// 47 | /// Update positioning information. 48 | /// 49 | /// 50 | /// The node's starting position. 51 | /// 52 | /// 53 | /// The node length. 54 | /// 55 | /// 56 | /// The . 57 | /// 58 | Compare IPositionAware.SetPos(Sprache.Position startPosition, int length) 59 | { 60 | SetPosition(startPosition, length); 61 | 62 | return this; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ComparisonKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 2 | { 3 | /// 4 | /// Represents a kind of MSBuild comparison expression. 5 | /// 6 | public enum ComparisonKind 7 | { 8 | /// 9 | /// Equality ("=="). 10 | /// 11 | Equality, 12 | 13 | /// 14 | /// Inequality ("!="). 15 | /// 16 | Inequality 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/EmptyListItem.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an empty MSBuild list item. 7 | /// 8 | public sealed class EmptyListItem 9 | : ExpressionNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public EmptyListItem() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.EmptyListItem; 22 | 23 | /// 24 | /// Update positioning information. 25 | /// 26 | /// 27 | /// The node's starting position. 28 | /// 29 | /// 30 | /// The node length. 31 | /// 32 | /// 33 | /// The . 34 | /// 35 | EmptyListItem IPositionAware.SetPos(Sprache.Position startPosition, int length) 36 | { 37 | SetPosition(startPosition, length); 38 | 39 | return this; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/Evaluate.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild evaluation expression. 7 | /// 8 | public class Evaluate 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public Evaluate() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.Evaluate; 22 | 23 | /// 24 | /// Is the evaluation expression valid (i.e. has exactly one child)? 25 | /// 26 | public override bool IsValid => Children.Count == 1 && base.IsValid; 27 | 28 | /// 29 | /// Update positioning information. 30 | /// 31 | /// 32 | /// The node's starting position. 33 | /// 34 | /// 35 | /// The node length. 36 | /// 37 | /// 38 | /// The . 39 | /// 40 | Evaluate IPositionAware.SetPos(Sprache.Position startPosition, int length) 41 | { 42 | SetPosition(startPosition, length); 43 | 44 | return this; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ExpressionContainerNode.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | using System.Collections.Immutable; 3 | using System; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 6 | { 7 | /// 8 | /// A node in an MSBuild expression tree that can have children. 9 | /// 10 | public abstract class ExpressionContainerNode 11 | : ExpressionNode, IPositionAware 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | protected ExpressionContainerNode() 17 | { 18 | } 19 | 20 | /// 21 | /// The node's children (if any). 22 | /// 23 | public ImmutableList Children { get; internal set; } = ImmutableList.Empty; 24 | 25 | /// 26 | /// Get the child expression at the specified index. 27 | /// 28 | /// 29 | /// The type of child expression to retrieve. 30 | /// 31 | /// 32 | /// The index of the child expression to retrieve. 33 | /// 34 | /// 35 | /// The child expression. 36 | /// 37 | protected TChild GetChild(int childIndex) 38 | where TChild : ExpressionNode 39 | { 40 | if (childIndex < 0 || childIndex >= Children.Count) 41 | throw new ArgumentOutOfRangeException(nameof(childIndex), childIndex, $"There is no child expression at index {childIndex}."); 42 | 43 | return (TChild)Children[childIndex]; 44 | } 45 | 46 | /// 47 | /// Update positioning information. 48 | /// 49 | /// 50 | /// The node's starting position. 51 | /// 52 | /// 53 | /// The node length. 54 | /// 55 | /// 56 | /// The . 57 | /// 58 | ExpressionContainerNode IPositionAware.SetPos(Sprache.Position startPosition, int length) 59 | { 60 | SetPosition(startPosition, length); 61 | 62 | return this; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ExpressionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | using System.Collections.Generic; 6 | using Utilities; 7 | 8 | /// 9 | /// Helper methods for working with s. 10 | /// 11 | static class ExpressionHelper 12 | { 13 | /// 14 | /// Perform post-parse processing on the node to ensure that s are populated and children are connected via the usual relationships (, , and ). 15 | /// 16 | /// 17 | /// The root node type. 18 | /// 19 | /// 20 | /// The root node. 21 | /// 22 | /// 23 | /// A used to map absolute node positions to line / column. 24 | /// 25 | /// 26 | /// The root node (enables inline use). 27 | /// 28 | public static TNode PostParse(this TNode root, TextPositions textPositions) 29 | where TNode : ExpressionNode 30 | { 31 | if (root == null) 32 | throw new System.ArgumentNullException(nameof(root)); 33 | 34 | if (textPositions == null) 35 | throw new System.ArgumentNullException(nameof(textPositions)); 36 | 37 | var positionCache = new Dictionary(); 38 | void SetRange(ExpressionNode node) 39 | { 40 | if (!positionCache.TryGetValue(node.AbsoluteStart, out Position start)) 41 | { 42 | start = textPositions.GetPosition(node.AbsoluteStart); 43 | positionCache.Add(node.AbsoluteStart, start); 44 | } 45 | 46 | if (!positionCache.TryGetValue(node.AbsoluteEnd, out Position end)) 47 | { 48 | end = textPositions.GetPosition(node.AbsoluteEnd); 49 | positionCache.Add(node.AbsoluteEnd, end); 50 | } 51 | 52 | node.Range = new Range(start, end); 53 | } 54 | 55 | SetRange(root); 56 | 57 | foreach (ExpressionContainerNode parent in root.DescendantNodes().OfType()) 58 | { 59 | SetRange(root); 60 | 61 | ExpressionNode previousSibling = null; 62 | foreach (ExpressionNode nextSibling in parent.Children) 63 | { 64 | SetRange(nextSibling); 65 | 66 | nextSibling.Parent = parent; 67 | nextSibling.PreviousSibling = previousSibling; 68 | if (previousSibling != null) 69 | previousSibling.NextSibling = nextSibling; 70 | 71 | previousSibling = nextSibling; 72 | } 73 | } 74 | 75 | return root; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ExpressionKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 2 | { 3 | /// 4 | /// Well-known kinds of MSBuild expression nodes. 5 | /// 6 | public enum ExpressionKind 7 | { 8 | /// 9 | /// The root of an expression tree. 10 | /// 11 | Root, 12 | 13 | /// 14 | /// A semicolon-delimited list of simple items. 15 | /// 16 | SimpleList, 17 | 18 | /// 19 | /// A simple list item. 20 | /// 21 | SimpleListItem, 22 | 23 | /// 24 | /// A simple list item separator. 25 | /// 26 | SimpleListSeparator, 27 | 28 | /// 29 | /// A semicolon-delimited list of expressions. 30 | /// 31 | List, 32 | 33 | /// 34 | /// Placeholder representing an empty slot in an expression list. 35 | /// 36 | EmptyListItem, 37 | 38 | /// 39 | /// A quoted string. 40 | /// 41 | QuotedString, 42 | 43 | /// 44 | /// An evaluation expression, "$(xxx)". 45 | /// 46 | Evaluate, 47 | 48 | /// 49 | /// An item group expression, "@(xxx)". 50 | /// 51 | ItemGroup, 52 | 53 | /// 54 | /// An item group transform expression, "@(xxx->'yyy')". 55 | /// 56 | ItemGroupTransform, 57 | 58 | /// 59 | /// An item metadata expression, "%(xxx.yyy)" or "%(yyy)". 60 | /// 61 | ItemMetadata, 62 | 63 | /// 64 | /// A function-call expression, "XXX(A,B,C)". 65 | /// 66 | FunctionCall, 67 | 68 | /// 69 | /// A comparison expression. 70 | /// 71 | Compare, 72 | 73 | /// 74 | /// A logical expression (e.g. AND, OR, NOT). 75 | /// 76 | Logical, 77 | 78 | /// 79 | /// A generic symbol. 80 | /// 81 | Symbol 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ExpressionList.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | using System.Collections.Generic; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 5 | { 6 | /// 7 | /// Represents an MSBuild list expression. 8 | /// 9 | public sealed class ExpressionList 10 | : ExpressionContainerNode, IPositionAware 11 | { 12 | /// 13 | /// Create a new . 14 | /// 15 | public ExpressionList() 16 | { 17 | } 18 | 19 | /// 20 | /// The node kind. 21 | /// 22 | public override ExpressionKind Kind => ExpressionKind.List; 23 | 24 | /// 25 | /// The list's items. 26 | /// 27 | public IEnumerable Items => Children; 28 | 29 | /// 30 | /// Update positioning information. 31 | /// 32 | /// 33 | /// The node's starting position. 34 | /// 35 | /// 36 | /// The node length. 37 | /// 38 | /// 39 | /// The . 40 | /// 41 | ExpressionList IPositionAware.SetPos(Sprache.Position startPosition, int length) 42 | { 43 | SetPosition(startPosition, length); 44 | 45 | return this; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ExpressionTree.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents the root of an MSBuild expression tree. 7 | /// 8 | public class ExpressionTree 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public ExpressionTree() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.Root; 22 | 23 | /// 24 | /// Update positioning information. 25 | /// 26 | /// 27 | /// The node's starting position. 28 | /// 29 | /// 30 | /// The node length. 31 | /// 32 | /// 33 | /// The . 34 | /// 35 | ExpressionTree IPositionAware.SetPos(Sprache.Position startPosition, int length) 36 | { 37 | SetPosition(startPosition, length); 38 | 39 | return this; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/FunctionCall.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 6 | { 7 | /// 8 | /// Represents an MSBuild function-call expression. 9 | /// 10 | public class FunctionCall 11 | : ExpressionContainerNode, IPositionAware 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | public FunctionCall() 17 | { 18 | } 19 | 20 | /// 21 | /// The node kind. 22 | /// 23 | public override ExpressionKind Kind => ExpressionKind.FunctionCall; 24 | 25 | /// 26 | /// The function name. 27 | /// 28 | public string Name { get; internal set; } 29 | 30 | /// 31 | /// The type of function represented by the function-call expression. 32 | /// 33 | public FunctionKind FunctionKind { get; internal set; } 34 | 35 | /// 36 | /// The target of the function-call (null, for functions). 37 | /// 38 | public ExpressionNode Target => FunctionKind != FunctionKind.Global ? Children[0] : null; 39 | 40 | /// 41 | /// The function-call's arguments (if any). 42 | /// 43 | public IEnumerable Arguments => FunctionKind != FunctionKind.Global ? Children.Skip(1) : Children; 44 | 45 | /// 46 | /// Update positioning information. 47 | /// 48 | /// 49 | /// The node's starting position. 50 | /// 51 | /// 52 | /// The node length. 53 | /// 54 | /// 55 | /// The . 56 | /// 57 | FunctionCall IPositionAware.SetPos(Sprache.Position startPosition, int length) 58 | { 59 | SetPosition(startPosition, length); 60 | 61 | return this; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/FunctionKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 2 | { 3 | /// 4 | /// Well-known kinds of function. 5 | /// 6 | public enum FunctionKind 7 | { 8 | /// 9 | /// A global function, "A(B,C)". 10 | /// 11 | Global, 12 | 13 | /// 14 | /// An instance method, "A.B(C,D)". 15 | /// 16 | InstanceMethod, 17 | 18 | /// 19 | /// A static method, "[A]::B(C,D)". 20 | /// 21 | StaticMethod 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ItemGroup.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild item group expression. 7 | /// 8 | public class ItemGroup 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public ItemGroup() 15 | { 16 | } 17 | 18 | /// 19 | /// Does te item group expression have a name? 20 | /// 21 | public bool HasName => !string.IsNullOrWhiteSpace(Name); 22 | 23 | /// 24 | /// The item group name. 25 | /// 26 | public string Name => Children.Count > 0 ? GetChild(0).Name : null; 27 | 28 | /// 29 | /// Is the item group expression valid? 30 | /// 31 | public override bool IsValid => HasName && base.IsValid; 32 | 33 | /// 34 | /// The node kind. 35 | /// 36 | public override ExpressionKind Kind => ExpressionKind.ItemGroup; 37 | 38 | /// 39 | /// Update positioning information. 40 | /// 41 | /// 42 | /// The node's starting position. 43 | /// 44 | /// 45 | /// The node length. 46 | /// 47 | /// 48 | /// The . 49 | /// 50 | ItemGroup IPositionAware.SetPos(Sprache.Position startPosition, int length) 51 | { 52 | SetPosition(startPosition, length); 53 | 54 | return this; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ItemGroupTransform.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild item group expression. 7 | /// 8 | public class ItemGroupTransform 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public ItemGroupTransform() 15 | { 16 | } 17 | 18 | /// 19 | /// Does te item group expression have a name? 20 | /// 21 | public bool HasName => !string.IsNullOrWhiteSpace(Name); 22 | 23 | /// 24 | /// The item group name. 25 | /// 26 | public string Name => Children.Count > 0 ? GetChild(0).Name : null; 27 | 28 | /// 29 | /// Does the expression have a body? 30 | /// 31 | public bool HasBody => Children.Count > 1; 32 | 33 | /// 34 | /// The item group transform's expression body (if any). 35 | /// 36 | public QuotedString Body => HasBody ? GetChild(1) : null; 37 | 38 | /// 39 | /// Does the expression declare a custom separator? 40 | /// 41 | public bool HasSeparator => Children.Count > 2; 42 | 43 | /// 44 | /// The item group transform's custom separator (if any). 45 | /// 46 | public QuotedStringLiteral Separator => HasSeparator ? GetChild(2) : null; 47 | 48 | /// 49 | /// Is the item group transform expression valid? 50 | /// 51 | public override bool IsValid => HasName && HasBody && base.IsValid; 52 | 53 | /// 54 | /// The node kind. 55 | /// 56 | public override ExpressionKind Kind => ExpressionKind.ItemGroup; 57 | 58 | /// 59 | /// Update positioning information. 60 | /// 61 | /// 62 | /// The node's starting position. 63 | /// 64 | /// 65 | /// The node length. 66 | /// 67 | /// 68 | /// The . 69 | /// 70 | ItemGroupTransform IPositionAware.SetPos(Sprache.Position startPosition, int length) 71 | { 72 | SetPosition(startPosition, length); 73 | 74 | return this; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ItemMetadata.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild item metadata expression. 7 | /// 8 | public class ItemMetadata 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new expression. 13 | /// 14 | public ItemMetadata() 15 | { 16 | } 17 | 18 | /// 19 | /// The name of the item metadata. 20 | /// 21 | public string Name 22 | { 23 | get 24 | { 25 | if (HasItemType) 26 | return GetChild(1).Name; 27 | 28 | if (HasName) 29 | return GetChild(0).Name; 30 | 31 | return string.Empty; 32 | } 33 | } 34 | 35 | /// 36 | /// The name of the item type on which the metadata is declared. 37 | /// 38 | /// 39 | /// May be empty for raw metadata expressions (e.g. "%(FullPath)" as opposed to "%(MyItem.FullPath)"). 40 | /// 41 | public string ItemType 42 | { 43 | get 44 | { 45 | if (HasItemType) 46 | return GetChild(0).Name; 47 | 48 | return string.Empty; 49 | } 50 | } 51 | 52 | /// 53 | /// Does the metadata expression specify an item type? 54 | /// 55 | public bool HasName => Children.Count > 0 && Children[0] is Symbol; 56 | 57 | /// 58 | /// Does the metadata expression specify an item type? 59 | /// 60 | public bool HasItemType => Children.Count > 1 && Children[0] is Symbol; 61 | 62 | /// 63 | /// Is the item metadata expression valid? 64 | /// 65 | public override bool IsValid => !string.IsNullOrWhiteSpace(Name) && base.IsValid; 66 | 67 | /// 68 | /// The node kind. 69 | /// 70 | public override ExpressionKind Kind => ExpressionKind.ItemMetadata; 71 | 72 | /// 73 | /// Update positioning information. 74 | /// 75 | /// 76 | /// The node's starting position. 77 | /// 78 | /// 79 | /// The node length. 80 | /// 81 | /// 82 | /// The . 83 | /// 84 | ItemMetadata IPositionAware.SetPos(Sprache.Position startPosition, int length) 85 | { 86 | SetPosition(startPosition, length); 87 | 88 | return this; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/ListSeparator.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents a list item separator with leading and trailing whitespace. 7 | /// 8 | public sealed class ListSeparator 9 | : ExpressionNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public ListSeparator() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.SimpleListSeparator; 22 | 23 | /// 24 | /// The offset, in characters, of the actual separator character from the of the . 25 | /// 26 | public int SeparatorOffset { get; internal set; } 27 | 28 | /// 29 | /// Update positioning information. 30 | /// 31 | /// 32 | /// The node's starting position. 33 | /// 34 | /// 35 | /// The node length. 36 | /// 37 | /// 38 | /// The . 39 | /// 40 | ListSeparator IPositionAware.SetPos(Sprache.Position startPosition, int length) 41 | { 42 | SetPosition(startPosition, length); 43 | 44 | return this; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/LogicalExpression.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild logical expression (e.g. AND, OR, NOT). 7 | /// 8 | public class LogicalExpression 9 | : ExpressionContainerNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public LogicalExpression() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.Logical; 22 | 23 | /// 24 | /// The kind of binary expression represented by the expression. 25 | /// 26 | public LogicalOperatorKind OperatorKind { get; internal set; } 27 | 28 | /// 29 | /// Is the expression a unary expression? 30 | /// 31 | public bool IsUnary => Children.Count == 1; 32 | 33 | /// 34 | /// Is the expression a binary expression? 35 | /// 36 | public bool IsBinary => Children.Count == 2; 37 | 38 | /// 39 | /// The left-hand operand. 40 | /// 41 | public ExpressionNode Left => IsBinary ? Children[0] : null; 42 | 43 | /// 44 | /// Get a string representation of the expression node. 45 | /// 46 | /// 47 | /// The string representation. 48 | /// 49 | public override string ToString() => $"MSBuild Logical ({OperatorKind}) expression @ {Range}"; 50 | 51 | /// 52 | /// The right-hand operand. 53 | /// 54 | public ExpressionNode Right 55 | { 56 | get 57 | { 58 | if (IsUnary) 59 | return Children[0]; 60 | 61 | if (IsBinary) 62 | return Children[1]; 63 | 64 | return null; 65 | } 66 | } 67 | 68 | /// 69 | /// Update positioning information. 70 | /// 71 | /// 72 | /// The node's starting position. 73 | /// 74 | /// 75 | /// The node length. 76 | /// 77 | /// 78 | /// The . 79 | /// 80 | LogicalExpression IPositionAware.SetPos(Sprache.Position startPosition, int length) 81 | { 82 | SetPosition(startPosition, length); 83 | 84 | return this; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/LogicalOperatorKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 2 | { 3 | /// 4 | /// Represents a kind of MSBuild logical operator. 5 | /// 6 | public enum LogicalOperatorKind 7 | { 8 | /// 9 | /// Logical-AND. 10 | /// 11 | And, 12 | 13 | /// 14 | /// Logical-OR. 15 | /// 16 | Or, 17 | 18 | /// 19 | /// Logical-NOT. 20 | /// 21 | Not 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/QuotedString.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 6 | { 7 | /// 8 | /// Represents an MSBuild quoted-string expression. 9 | /// 10 | public class QuotedString 11 | : ExpressionContainerNode, IPositionAware 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | public QuotedString() 17 | { 18 | } 19 | 20 | /// 21 | /// The node kind. 22 | /// 23 | public override ExpressionKind Kind => ExpressionKind.QuotedString; 24 | 25 | /// 26 | /// Quoted strings are never virtual. 27 | /// 28 | public override bool IsVirtual => false; 29 | 30 | /// 31 | /// Evaluation expressions (if any) contained in the string. 32 | /// 33 | public IEnumerable Evaluations => Children.OfType(); 34 | 35 | /// 36 | /// The quoted string's textual content (without evaluation expressions). 37 | /// 38 | public virtual string StringContent => string.Join("", 39 | Children.OfType().Select( 40 | stringContent => stringContent.Content 41 | ) 42 | ); 43 | 44 | /// 45 | /// Update positioning information. 46 | /// 47 | /// 48 | /// The node's starting position. 49 | /// 50 | /// 51 | /// The node length. 52 | /// 53 | /// 54 | /// The . 55 | /// 56 | QuotedString IPositionAware.SetPos(Sprache.Position startPosition, int length) 57 | { 58 | SetPosition(startPosition, length); 59 | 60 | return this; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/QuotedStringLiteral.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild quoted-string literal expression. 7 | /// 8 | /// 9 | /// Quoted strings can contain sub-expressions, but quoted string literals cannot. 10 | /// 11 | public sealed class QuotedStringLiteral 12 | : QuotedString, IPositionAware 13 | { 14 | /// 15 | /// Create a new . 16 | /// 17 | public QuotedStringLiteral() 18 | { 19 | } 20 | 21 | /// 22 | /// The node kind. 23 | /// 24 | public override ExpressionKind Kind => ExpressionKind.QuotedString; 25 | 26 | /// 27 | /// The string content. 28 | /// 29 | public string Content { get; set; } 30 | 31 | /// 32 | /// The quoted string's textual content. 33 | /// 34 | public override string StringContent => Content; 35 | 36 | /// 37 | /// Get a string representation of the expression node. 38 | /// 39 | /// 40 | /// The string representation. 41 | /// 42 | public override string ToString() => $"MSBuild QuotedStringLiteral @ {Range}"; 43 | 44 | /// 45 | /// Update positioning information. 46 | /// 47 | /// 48 | /// The node's starting position. 49 | /// 50 | /// 51 | /// The node length. 52 | /// 53 | /// 54 | /// The . 55 | /// 56 | QuotedStringLiteral IPositionAware.SetPos(Sprache.Position startPosition, int length) 57 | { 58 | SetPosition(startPosition, length); 59 | 60 | return this; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/SimpleList.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 6 | { 7 | /// 8 | /// Represents a simple MSBuild list expression. 9 | /// 10 | public sealed class SimpleList 11 | : ExpressionContainerNode, IPositionAware 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | public SimpleList() 17 | { 18 | } 19 | 20 | /// 21 | /// The node kind. 22 | /// 23 | public override ExpressionKind Kind => ExpressionKind.SimpleList; 24 | 25 | /// 26 | /// The list's items. 27 | /// 28 | public IEnumerable Items => Children.OfType(); 29 | 30 | /// 31 | /// Update positioning information. 32 | /// 33 | /// 34 | /// The node's starting position. 35 | /// 36 | /// 37 | /// The node length. 38 | /// 39 | /// 40 | /// The . 41 | /// 42 | SimpleList IPositionAware.SetPos(Sprache.Position startPosition, int length) 43 | { 44 | SetPosition(startPosition, length); 45 | 46 | return this; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/SimpleListItem.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents a simple MSBuild list item. 7 | /// 8 | public sealed class SimpleListItem 9 | : ExpressionNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public SimpleListItem() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.SimpleListItem; 22 | 23 | /// 24 | /// The item value. 25 | /// 26 | public string Value { get; internal set; } 27 | 28 | /// 29 | /// Update positioning information. 30 | /// 31 | /// 32 | /// The node's starting position. 33 | /// 34 | /// 35 | /// The node length. 36 | /// 37 | /// 38 | /// The . 39 | /// 40 | SimpleListItem IPositionAware.SetPos(Sprache.Position startPosition, int length) 41 | { 42 | SetPosition(startPosition, length); 43 | 44 | return this; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/StringContent.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents a run of contiguous characters in an MSBuild quoted-string literal expression. 7 | /// 8 | /// 9 | /// 'Foo $(XXX)' will be parsed as StringContent("Foo ") and Evaluation(Symbol("XXX")). 10 | /// 11 | public sealed class StringContent 12 | : ExpressionNode, IPositionAware 13 | { 14 | /// 15 | /// Create a new . 16 | /// 17 | public StringContent() 18 | { 19 | } 20 | 21 | /// 22 | /// The node kind. 23 | /// 24 | public override ExpressionKind Kind => ExpressionKind.QuotedString; 25 | 26 | /// 27 | /// String content is never virtual. 28 | /// 29 | public override bool IsVirtual => false; 30 | 31 | /// 32 | /// The string content. 33 | /// 34 | public string Content { get; set; } 35 | 36 | /// 37 | /// Get a string representation of the expression node. 38 | /// 39 | /// 40 | /// The string representation. 41 | /// 42 | public override string ToString() => $"MSBuild StringContent @ {Range}"; 43 | 44 | /// 45 | /// Update positioning information. 46 | /// 47 | /// 48 | /// The node's starting position. 49 | /// 50 | /// 51 | /// The node length. 52 | /// 53 | /// 54 | /// The . 55 | /// 56 | StringContent IPositionAware.SetPos(Sprache.Position startPosition, int length) 57 | { 58 | SetPosition(startPosition, length); 59 | 60 | return this; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildExpressions/Symbol.cs: -------------------------------------------------------------------------------- 1 | using Sprache; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel.MSBuildExpressions 4 | { 5 | /// 6 | /// Represents an MSBuild comparison expression. 7 | /// 8 | public class Symbol 9 | : ExpressionNode, IPositionAware 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | public Symbol() 15 | { 16 | } 17 | 18 | /// 19 | /// The node kind. 20 | /// 21 | public override ExpressionKind Kind => ExpressionKind.Symbol; 22 | 23 | /// 24 | /// The symbol's name. 25 | /// 26 | public string Name { get; internal set; } = string.Empty; 27 | 28 | /// 29 | /// The symbol's namespace. 30 | /// 31 | public string Namespace { get; set; } = string.Empty; 32 | 33 | /// 34 | /// The symbol's fully-qualified name. 35 | /// 36 | public string FullName => IsQualified ? string.Format("{0}.{1}", Namespace, Name) : Name; 37 | 38 | /// 39 | /// Is the symbol qualified (i.e. does it have a namespace)? 40 | /// 41 | public bool IsQualified => !string.IsNullOrWhiteSpace(Namespace); 42 | 43 | /// 44 | /// Is the symbol valid? 45 | /// 46 | public override bool IsValid => !string.IsNullOrWhiteSpace(Name) && base.IsValid; 47 | 48 | /// 49 | /// Get a string representation of the expression node. 50 | /// 51 | /// 52 | /// The string representation. 53 | /// 54 | public override string ToString() => $"MSBuild Symbol ({FullName}) @ {Range}"; 55 | 56 | /// 57 | /// Update positioning information. 58 | /// 59 | /// 60 | /// The node's starting position. 61 | /// 62 | /// 63 | /// The node length. 64 | /// 65 | /// 66 | /// The . 67 | /// 68 | Symbol IPositionAware.SetPos(Sprache.Position startPosition, int length) 69 | { 70 | SetPosition(startPosition, length); 71 | 72 | return this; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildImport.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 7 | { 8 | /// 9 | /// An import in an MSBuild project. 10 | /// 11 | public class MSBuildImport 12 | : MSBuildObject> 13 | { 14 | /// 15 | /// Create a new . 16 | /// 17 | /// 18 | /// The underlying MSBuild . 19 | /// 20 | /// 21 | /// An representing the import's XML element. 22 | /// 23 | public MSBuildImport(IReadOnlyList imports, XSElement importElement) 24 | : base(imports, importElement) 25 | { 26 | } 27 | 28 | /// 29 | /// The import name. 30 | /// 31 | public override string Name => Imports[0].ImportingElement.Project; 32 | 33 | /// 34 | /// The kind of MSBuild object represented by the . 35 | /// 36 | public override MSBuildObjectKind Kind => MSBuildObjectKind.Import; 37 | 38 | /// 39 | /// The full path of the file where the import is declared. 40 | /// 41 | public override string SourceFile => Imports[0].ImportingElement.Location.File; 42 | 43 | /// 44 | /// The underlying . 45 | /// 46 | public IReadOnlyList Imports => UnderlyingObject; 47 | 48 | /// 49 | /// The import's declaring element. 50 | /// 51 | public XSElement Element => (XSElement)Xml; 52 | 53 | /// 54 | /// The import's "Project" attribute. 55 | /// 56 | public XSAttribute ProjectAttribute => Element["Project"]; 57 | 58 | /// 59 | /// The underlying . 60 | /// 61 | public ProjectImportElement ImportingElement => Imports[0].ImportingElement; 62 | 63 | /// 64 | /// The imported project file names (only returns imported projects that have file names). 65 | /// 66 | public IEnumerable ImportedProjectFiles => Imports.Select(import => import.ImportedProject.ProjectFileLocation.File).Where(projectFile => projectFile != string.Empty); 67 | 68 | /// 69 | /// The imported . 70 | /// 71 | public IEnumerable ImportedProjectRoots => Imports.Select(import => import.ImportedProject); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildModelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 8 | { 9 | /// 10 | /// Extension methods for working with MSBuild models. 11 | /// 12 | public static class MSBuildModelExtensions 13 | { 14 | /// 15 | /// Enumerate the names of all tasks available for use in the project. 16 | /// 17 | /// 18 | /// The MSBuild . 19 | /// 20 | /// 21 | /// A sequence of task names. 22 | /// 23 | public static IEnumerable GetAvailableTaskNames(this Project project) 24 | { 25 | if (project == null) 26 | throw new ArgumentNullException(nameof(project)); 27 | 28 | return project.GetAllUsingTasks().Select( 29 | usingTask => usingTask.TaskName 30 | ); 31 | } 32 | 33 | /// 34 | /// Recursively enumerate all s in the project and any projects that it imports. 35 | /// 36 | /// 37 | /// The MSBuild . 38 | /// 39 | /// 40 | /// A sequence of s. 41 | /// 42 | public static IEnumerable GetAllUsingTasks(this Project project) 43 | { 44 | if (project == null) 45 | throw new ArgumentNullException(nameof(project)); 46 | 47 | return 48 | project.Xml.UsingTasks.Concat( 49 | project.Imports.SelectMany( 50 | import => import.ImportedProject.UsingTasks 51 | ) 52 | ); 53 | } 54 | 55 | 56 | /// 57 | /// Convert the MSBuild to its native equivalent. 58 | /// 59 | /// 60 | /// The to convert. 61 | /// 62 | /// 63 | /// The equivalent . 64 | /// 65 | public static Position ToNative(this ElementLocation location) 66 | { 67 | if (location == null) 68 | return Position.Zero; 69 | 70 | if (location.Line == 0) 71 | return Position.Invalid; 72 | 73 | return new Position(location.Line, location.Column); 74 | } 75 | 76 | /// 77 | /// Get the condition (if any) declared on the element or one of its ancestors. 78 | /// 79 | /// 80 | /// The element. 81 | /// 82 | /// 83 | /// The condition, or an empty string if no condition is present on the element or one of its ancestors. 84 | /// 85 | public static string FindCondition(this ProjectElement projectElement) 86 | { 87 | if (projectElement == null) 88 | throw new ArgumentNullException(nameof(projectElement)); 89 | 90 | ProjectElement currentElement = projectElement; 91 | while (currentElement != null) 92 | { 93 | if (!string.IsNullOrWhiteSpace(currentElement.Condition)) 94 | return currentElement.Condition; 95 | 96 | currentElement = currentElement.Parent; 97 | } 98 | 99 | return string.Empty; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildObjectKind.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | /// 7 | /// A type of MSBuild object. 8 | /// 9 | public enum MSBuildObjectKind 10 | { 11 | /// 12 | /// An object in an invalid MSBuild project. 13 | /// 14 | Invalid = 0, 15 | 16 | /// 17 | /// A target () in an MSBuild project. 18 | /// 19 | Target = 1, 20 | 21 | /// 22 | /// An item () in an MSBuild project. 23 | /// 24 | Item = 2, 25 | 26 | /// 27 | /// An item () in an MSBuild project whose condition evaluates as false. 28 | /// 29 | UnusedItem = 3, 30 | 31 | /// 32 | /// A property () in an MSBuild project. 33 | /// 34 | Property = 4, 35 | 36 | /// 37 | /// An unused property ( without a corresponding ) in an MSBuild project. 38 | /// 39 | UnusedProperty = 5, 40 | 41 | /// 42 | /// A project import () in an MSBuild project. 43 | /// 44 | Import = 6, 45 | 46 | /// 47 | /// An unresolved import ( without a corresponding ) in an MSBuild project. 48 | /// 49 | UnresolvedImport = 7, 50 | 51 | /// 52 | /// An SDK-style project import () in an MSBuild project. 53 | /// 54 | SdkImport = 8, 55 | 56 | /// 57 | /// An unresolved SDK-style import ( without a corresponding ) in an MSBuild project. 58 | /// 59 | UnresolvedSdkImport = 9 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildProperty.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | using System; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 6 | { 7 | /// 8 | /// A property in an MSBuild project. 9 | /// 10 | public sealed class MSBuildProperty 11 | : MSBuildObject 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | /// 17 | /// The underlying MSBuild . 18 | /// 19 | /// 20 | /// The that results in the underlying MSBuild 's current value. 21 | /// 22 | /// 23 | /// An representing the property's XML element. 24 | /// 25 | public MSBuildProperty(ProjectProperty property, ProjectPropertyElement declaringXml, XSElement propertyElement) 26 | : base(property, propertyElement) 27 | { 28 | if (declaringXml == null) 29 | throw new ArgumentNullException(nameof(declaringXml)); 30 | 31 | DeclaringXml = declaringXml; 32 | } 33 | 34 | /// 35 | /// The property name. 36 | /// 37 | public override string Name => Property.Name; 38 | 39 | /// 40 | /// The kind of MSBuild object represented by the . 41 | /// 42 | public override MSBuildObjectKind Kind => MSBuildObjectKind.Property; 43 | 44 | /// 45 | /// The full path of the file where the property is declared. 46 | /// 47 | public override string SourceFile => Property.Xml.Location.File; 48 | 49 | /// 50 | /// The property's declaring element. 51 | /// 52 | public XSElement Element => (XSElement)Xml; 53 | 54 | /// 55 | /// The property's evaluated value. 56 | /// 57 | public string Value => Property.EvaluatedValue; 58 | 59 | /// 60 | /// The property's raw (unevaluated) value. 61 | /// 62 | public string RawValue => Property.UnevaluatedValue; 63 | 64 | /// 65 | /// The underlying MSBuild . 66 | /// 67 | public ProjectProperty Property => UnderlyingObject; 68 | 69 | /// 70 | /// The that results in the underlying MSBuild 's current value. 71 | /// 72 | public ProjectPropertyElement DeclaringXml { get; } 73 | 74 | /// 75 | /// Has the property value been overridden elsewhere? 76 | /// 77 | public bool IsOverridden => Property.Xml != DeclaringXml; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildSdkImport.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 7 | { 8 | /// 9 | /// An SDK-style import in an MSBuild project. 10 | /// 11 | public class MSBuildSdkImport 12 | : MSBuildObject> 13 | { 14 | /// 15 | /// Create a new . 16 | /// 17 | /// 18 | /// A read-only list of underlying MSBuild s representing the imports resulting from the SDK import. 19 | /// 20 | /// 21 | /// An representing the import's "Sdk" attribute. 22 | /// 23 | public MSBuildSdkImport(IReadOnlyList imports, XSAttribute sdkAttribute) 24 | : base(imports, sdkAttribute) 25 | { 26 | } 27 | 28 | /// 29 | /// The import name. 30 | /// 31 | public override string Name => Imports[0].ImportingElement.Sdk; 32 | 33 | /// 34 | /// The kind of MSBuild object represented by the . 35 | /// 36 | public override MSBuildObjectKind Kind => MSBuildObjectKind.SdkImport; 37 | 38 | /// 39 | /// The full path of the file where the import is declared. 40 | /// 41 | public override string SourceFile => Imports[0].ImportingElement.Location.File; 42 | 43 | /// 44 | /// The import's "Sdk" attribute. 45 | /// 46 | public XSAttribute Attribute => (XSAttribute)Xml; 47 | 48 | /// 49 | /// The underlying s. 50 | /// 51 | public IReadOnlyList Imports => UnderlyingObject; 52 | 53 | /// 54 | /// The underlying . 55 | /// 56 | public ProjectImportElement ImportingElement => Imports[0].ImportingElement; 57 | 58 | /// 59 | /// The imported project file names (only returns imported projects that have file names). 60 | /// 61 | public IEnumerable ImportedProjectFiles => Imports.Select(import => import.ImportedProject.ProjectFileLocation.File).Where(projectFile => projectFile != string.Empty); 62 | 63 | /// 64 | /// The imported . 65 | /// 66 | public IEnumerable ImportedProjectRoots => Imports.Select(import => import.ImportedProject); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildTarget.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// A target in an MSBuild project. 7 | /// 8 | public sealed class MSBuildTarget 9 | : MSBuildObject 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The underlying MSBuild . 16 | /// 17 | /// 18 | /// An representing the target's XML element. 19 | /// 20 | public MSBuildTarget(ProjectTargetElement target, XSElement element) 21 | : base(target, element) 22 | { 23 | } 24 | 25 | /// 26 | /// The target name. 27 | /// 28 | public override string Name => Target.Name; 29 | 30 | /// 31 | /// The kind of MSBuild object represented by the . 32 | /// 33 | public override MSBuildObjectKind Kind => MSBuildObjectKind.Target; 34 | 35 | /// 36 | /// The full path of the file where the target is declared. 37 | /// 38 | public override string SourceFile => Target.Location.File; 39 | 40 | /// 41 | /// The target's declaring element. 42 | /// 43 | public XSElement Element => (XSElement)Xml; 44 | 45 | /// 46 | /// The underlying MSBuild . 47 | /// 48 | public ProjectTargetElement Target => UnderlyingObject; 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildTaskScanner.Metadata.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 6 | { 7 | /// 8 | /// Metadata for an assembly containing MSBuild tasks. 9 | /// 10 | public class MSBuildTaskAssemblyMetadata 11 | { 12 | /// 13 | /// The assembly's full name. 14 | /// 15 | [JsonProperty("assemblyName")] 16 | public string AssemblyName { get; set; } 17 | 18 | /// 19 | /// The full path to the assembly file. 20 | /// 21 | [JsonProperty("assemblyPath")] 22 | public string AssemblyPath { get; set; } 23 | 24 | /// 25 | /// The assembly file's timestamp. 26 | /// 27 | [JsonProperty("timestampUtc")] 28 | public DateTime TimestampUtc { get; set; } 29 | 30 | /// 31 | /// Tasks defined in the assembly. 32 | /// 33 | [JsonProperty("tasks", ObjectCreationHandling = ObjectCreationHandling.Reuse)] 34 | public List Tasks { get; } = new List(); 35 | } 36 | 37 | /// 38 | /// Metadata for an MSBuild task. 39 | /// 40 | public class MSBuildTaskMetadata 41 | { 42 | /// 43 | /// The task name. 44 | /// 45 | [JsonProperty("taskName")] 46 | public string Name { get; set; } 47 | 48 | /// 49 | /// The full name of the type that implements the task. 50 | /// 51 | [JsonProperty("typeName")] 52 | public string TypeName { get; set; } 53 | 54 | /// 55 | /// The task parameters (if any). 56 | /// 57 | [JsonProperty("parameters", ObjectCreationHandling = ObjectCreationHandling.Reuse)] 58 | public List Parameters { get; } = new List(); 59 | } 60 | 61 | /// 62 | /// Metadata for a parameter of an MSBuild task. 63 | /// 64 | public class MSBuildTaskParameterMetadata 65 | { 66 | /// 67 | /// The parameter name. 68 | /// 69 | [JsonProperty("parameterName")] 70 | public string Name { get; set; } 71 | 72 | /// 73 | /// The full name of the parameter's data type. 74 | /// 75 | [JsonProperty("parameterType")] 76 | public string TypeName { get; set; } 77 | 78 | /// 79 | /// Is the parameter type an enum? 80 | /// 81 | [JsonIgnore] 82 | public bool IsEnum => EnumMemberNames != null; 83 | 84 | /// 85 | /// If the parameter type is an , the names of the values that the parameter can contain. 86 | /// 87 | [JsonProperty("enum", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 88 | public List EnumMemberNames { get; set; } 89 | 90 | /// 91 | /// Is the parameter mandatory? 92 | /// 93 | [JsonProperty("required", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 94 | public bool IsRequired { get; set; } 95 | 96 | /// 97 | /// Is the parameter an output parameter? 98 | /// 99 | [JsonProperty("output", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 100 | public bool IsOutput { get; set; } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildUnresolvedImport.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// An unresolved regular-style import in an MSBuild project. 7 | /// 8 | public class MSBuildUnresolvedImport 9 | : MSBuildObject 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The underlying MSBuild . 16 | /// 17 | /// 18 | /// An representing the import's XML element. 19 | /// 20 | public MSBuildUnresolvedImport(ProjectImportElement import, XSElement importElement) 21 | : base(import, importElement) 22 | { 23 | } 24 | 25 | /// 26 | /// The import name. 27 | /// 28 | public override string Name => ImportingElement.Project; 29 | 30 | /// 31 | /// The kind of MSBuild object represented by the . 32 | /// 33 | public override MSBuildObjectKind Kind => MSBuildObjectKind.UnresolvedImport; 34 | 35 | /// 36 | /// The full path of the file where the import is declared. 37 | /// 38 | public override string SourceFile => ImportingElement.Location.File; 39 | 40 | /// 41 | /// The import's declaring element. 42 | /// 43 | public XSElement Element => (XSElement)Xml; 44 | 45 | /// 46 | /// The imported path. 47 | /// 48 | public string Project => ImportingElement.Project; 49 | 50 | /// 51 | /// The unresolved item's unevaluated condition. 52 | /// 53 | public string Condition => ImportingElement.FindCondition(); 54 | 55 | /// 56 | /// The import's "Project" attribute. 57 | /// 58 | public XSAttribute ProjectAttribute => Element["Project"]; 59 | 60 | /// 61 | /// The underlying . 62 | /// 63 | public ProjectImportElement ImportingElement => UnderlyingObject; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildUnresolvedSdkImport.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// An unresolved SDK-style import in an MSBuild project. 7 | /// 8 | public class MSBuildUnresolvedSdkImport 9 | : MSBuildObject 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The underlying MSBuild . 16 | /// 17 | /// 18 | /// An representing the import's "Sdk" attribute. 19 | /// 20 | public MSBuildUnresolvedSdkImport(ProjectImportElement import, XSAttribute sdkAttribute) 21 | : base(import, sdkAttribute) 22 | { 23 | } 24 | 25 | /// 26 | /// The import name. 27 | /// 28 | public override string Name => ImportingElement.Project; 29 | 30 | /// 31 | /// The kind of MSBuild object represented by the . 32 | /// 33 | public override MSBuildObjectKind Kind => MSBuildObjectKind.UnresolvedSdkImport; 34 | 35 | /// 36 | /// The full path of the file where the import is declared. 37 | /// 38 | public override string SourceFile => ImportingElement.Location.File; 39 | 40 | /// 41 | /// The imported SDK. 42 | /// 43 | public string Sdk => ImportingElement.Sdk; 44 | 45 | /// 46 | /// The unresolved item's unevaluated condition. 47 | /// 48 | public string Condition => ImportingElement.FindCondition(); 49 | 50 | /// 51 | /// The import's "Sdk" attribute. 52 | /// 53 | public XSAttribute SdkAttribute => (XSAttribute)Xml; 54 | 55 | /// 56 | /// The underlying . 57 | /// 58 | public ProjectImportElement ImportingElement => UnderlyingObject; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/MSBuildUnusedProperty.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Construction; 2 | using Microsoft.Build.Evaluation; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | /// 7 | /// An unused property (i.e. a with no corresponding ) in an MSBuild project, usually because the condition evaluates to false. 8 | /// 9 | public sealed class MSBuildUnusedProperty 10 | : MSBuildObject 11 | { 12 | /// 13 | /// Create a new . 14 | /// 15 | /// 16 | /// An representing the MSBuild property. 17 | /// 18 | /// 19 | /// An representing the property's declaring XML element. 20 | /// 21 | public MSBuildUnusedProperty(ProjectPropertyElement propertyElement, XSElement declaringElement) 22 | : base(propertyElement, declaringElement) 23 | { 24 | } 25 | 26 | /// 27 | /// The property name. 28 | /// 29 | public override string Name => PropertyElement.Name; 30 | 31 | /// 32 | /// The that declares the property. 33 | /// 34 | public XSElement Element => (XSElement)base.Xml; 35 | 36 | /// 37 | /// The kind of MSBuild object represented by the . 38 | /// 39 | public override MSBuildObjectKind Kind => MSBuildObjectKind.UnusedProperty; 40 | 41 | /// 42 | /// The full path of the file where the target is declared. 43 | /// 44 | public override string SourceFile => PropertyElement.Location.File; 45 | 46 | /// 47 | /// The property's raw (unevaluated) value. 48 | /// 49 | public string Value => PropertyElement.Value; 50 | 51 | /// 52 | /// The underlying MSBuild . 53 | /// 54 | public ProjectPropertyElement PropertyElement => UnderlyingObject; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("MSBuildProjectTools.LanguageServer.Engine.Tests")] 4 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/WellKnownElementPaths.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 2 | { 3 | /// 4 | /// s representing well-known elements in MSBuild projects. 5 | /// 6 | public static class WellKnownElementPaths 7 | { 8 | /// 9 | /// The absolute path of the root "Project" element. 10 | /// 11 | public static readonly XSPath Project = XSPath.Parse("/Project"); 12 | 13 | /// 14 | /// The relative path that represents a "PropertyGroup" element (static or dynamic). 15 | /// 16 | public static readonly XSPath PropertyGroup = XSPath.Parse("PropertyGroup"); 17 | 18 | /// 19 | /// The relative path that represents a dynamic "PropertyGroup" element. 20 | /// 21 | public static readonly XSPath DynamicPropertyGroup = XSPath.Parse("Target/PropertyGroup"); 22 | 23 | /// 24 | /// The relative path that represents a "ItemGroup" element (static or dynamic). 25 | /// 26 | public static readonly XSPath ItemGroup = XSPath.Parse("ItemGroup"); 27 | 28 | /// 29 | /// The relative path that represents any direct child of an "ItemGroup" element (static or dynamic). 30 | /// 31 | public static readonly XSPath Item = ItemGroup + "*"; 32 | 33 | /// 34 | /// The relative path that represents a "PackageReference" item element. 35 | /// 36 | public static readonly XSPath PackageReference = ItemGroup + "PackageReference"; 37 | 38 | /// 39 | /// The relative path that represents a "PackageVersion" item element. 40 | /// 41 | public static readonly XSPath PackageVersion = ItemGroup + "PackageVersion"; 42 | 43 | /// 44 | /// The relative path that represents a "DotNetCliToolReference" item element. 45 | /// 46 | public static readonly XSPath DotNetCliToolReference = ItemGroup + "DotNetCliToolReference"; 47 | 48 | /// 49 | /// The absolute path that represents a "Target" element. 50 | /// 51 | public static readonly XSPath Target = Project + "Target"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.MSBuild/XmlLocationMSBuildExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | using MSBuildExpressions; 6 | 7 | /// 8 | /// MSBuild-related extension methods for . 9 | /// 10 | public static class XmlLocationMSBuildExtensions 11 | { 12 | /// 13 | /// Does the location represent an MSBuild expression? 14 | /// 15 | /// 16 | /// The XML location. 17 | /// 18 | /// 19 | /// Receives the expression (if any) at the location. 20 | /// 21 | /// 22 | /// The that contains the expression. 23 | /// 24 | /// 25 | /// true, if the location represents an MSBuild expression; otherwise, false. 26 | /// 27 | public static bool IsExpression(this XmlLocation location, out ExpressionNode expression, out Range expressionRange) 28 | { 29 | if (location == null) 30 | throw new ArgumentNullException(nameof(location)); 31 | 32 | expression = null; 33 | expressionRange = Range.Zero; 34 | 35 | string expressionText; 36 | Position expressionStartPosition; 37 | if (location.IsElementText(out XSElementText text)) 38 | { 39 | expressionText = text.Text; 40 | expressionStartPosition = text.Range.Start; 41 | } 42 | else if (location.IsAttributeValue(out XSAttribute attribute)) 43 | { 44 | expressionText = attribute.Value; 45 | expressionStartPosition = attribute.ValueRange.Start; 46 | } 47 | else if (location.IsWhitespace(out XSWhitespace whitespace)) 48 | { 49 | expressionText = string.Empty; 50 | expressionStartPosition = whitespace.Range.Start; 51 | } 52 | else 53 | return false; 54 | 55 | if (!MSBuildExpression.TryParse(expressionText, out ExpressionTree expressionTree)) 56 | return false; 57 | 58 | Position expressionPosition = location.Position.RelativeTo(expressionStartPosition); 59 | 60 | ExpressionNode expressionAtPosition = expressionTree.FindDeepestNodeAt(expressionPosition); 61 | if (expressionAtPosition == null) 62 | return false; 63 | 64 | expression = expressionAtPosition; 65 | expressionRange = expressionAtPosition.Range.WithOrigin(expressionStartPosition); 66 | 67 | return true; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/LanguageServer.SemanticModel.Xml.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | MSBuildProjectTools.LanguageServer.SemanticModel.Xml 4 | MSBuildProjectTools.LanguageServer.SemanticModel 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/PaddingType.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 2 | { 3 | /// 4 | /// The type of padding to apply. 5 | /// 6 | public enum PaddingType 7 | { 8 | /// 9 | /// No padding. 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// Add padding before. 15 | /// 16 | Leading = 1, 17 | 18 | /// 19 | /// Add padding after. 20 | /// 21 | Trailing = 2 22 | } 23 | 24 | /// 25 | /// Extension methods for padding strings. 26 | /// 27 | public static class PaddingExtensions 28 | { 29 | /// 30 | /// Add padding to the string. 31 | /// 32 | /// 33 | /// The string. 34 | /// 35 | /// 36 | /// The type of padding to add. 37 | /// 38 | /// 39 | /// The padded string. 40 | /// 41 | public static string WithPadding(this string str, PaddingType paddingType) 42 | { 43 | if (str == null) 44 | return null; 45 | 46 | if (paddingType == PaddingType.Leading) 47 | return " " + str; 48 | 49 | if (paddingType == PaddingType.Trailing) 50 | return str + " "; 51 | 52 | return str; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSElementText.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | using System; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | /// 7 | /// Represents text within an XML element's content. 8 | /// 9 | public class XSElementText 10 | : XSNode 11 | { 12 | /// 13 | /// The text's path within the XML. 14 | /// 15 | readonly XSPath _path; 16 | 17 | /// 18 | /// Create new . 19 | /// 20 | /// 21 | /// The represented by the . 22 | /// 23 | /// 24 | /// The , within the source text, spanned by the text. 25 | /// 26 | /// 27 | /// The element whose content includes the text. 28 | /// 29 | public XSElementText(XmlTextSyntax textNode, Range range, XSElement element) 30 | : base(textNode, range) 31 | { 32 | if (element == null) 33 | throw new ArgumentNullException(nameof(element)); 34 | 35 | Element = element; 36 | _path = Element.Path + Name; 37 | } 38 | 39 | /// 40 | /// The text's path within the XML. 41 | /// 42 | public override XSPath Path => _path; 43 | 44 | /// 45 | /// The represented by the . 46 | /// 47 | public XmlTextSyntax TextNode => SyntaxNode; 48 | 49 | /// 50 | /// The text. 51 | /// 52 | public string Text => TextNode.ToFullString(); 53 | 54 | /// 55 | /// The element whose content includes the text. 56 | /// 57 | public XSElement Element { get; } 58 | 59 | /// 60 | /// The kind of XML node represented by the . 61 | /// 62 | public override XSNodeKind Kind => XSNodeKind.Text; 63 | 64 | /// 65 | /// The node name. 66 | /// 67 | public override string Name => "#text"; 68 | 69 | /// 70 | /// Does the represent valid XML? 71 | /// 72 | public override bool IsValid => true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSElementWithContent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Represents an XML element with content. 7 | /// 8 | public class XSElementWithContent 9 | : XSElement 10 | { 11 | /// 12 | /// The range, within the source text, spanned by the node. 13 | /// 14 | /// 15 | /// The represented by the . 16 | /// 17 | /// 18 | /// The , within the source text, spanned by the element and its content. 19 | /// 20 | /// 21 | /// The range, within the source text, spanned by the element's name. 22 | /// 23 | /// 24 | /// The , within the source text, spanned by the element's opening tag. 25 | /// 26 | /// 27 | /// The range, within the source text, spanned by the element's attributes. 28 | /// 29 | /// 30 | /// The , within the source text, spanned by the element's content. 31 | /// 32 | /// 33 | /// The , within the source text, spanned by the element's closing tag. 34 | /// 35 | /// 36 | /// The 's parent element (if any). 37 | /// 38 | public XSElementWithContent(XmlElementSyntax element, Range range, Range nameRange, Range openingTagRange, Range attributesRange, Range contentRange, Range closingTagRange, XSElement parent) 39 | : base(element, range, nameRange, attributesRange, parent) 40 | { 41 | OpeningTagRange = openingTagRange; 42 | ContentRange = contentRange; 43 | ClosingTagRange = closingTagRange; 44 | } 45 | 46 | /// 47 | /// The represented by the . 48 | /// 49 | public new XmlElementSyntax ElementNode => (XmlElementSyntax)SyntaxNode; 50 | 51 | /// 52 | /// The , within the source text, spanned by the element's opening tag. 53 | /// 54 | public Range OpeningTagRange { get; } 55 | 56 | /// 57 | /// The , within the source text, spanned by the element's content. 58 | /// 59 | public Range ContentRange { get; } 60 | 61 | /// 62 | /// The , within the source text, spanned by the element's closing tag. 63 | /// 64 | public Range ClosingTagRange { get; } 65 | 66 | /// 67 | /// The kind of XML node represented by the . 68 | /// 69 | public override XSNodeKind Kind => XSNodeKind.Element; 70 | 71 | /// 72 | /// Does the represent valid XML? 73 | /// 74 | public override bool IsValid => true; 75 | 76 | /// 77 | /// Does the have any content (besides attributes)? 78 | /// 79 | public override bool HasContent => Content.Count > 0; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSEmptyElement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Represents an empty XML element. 7 | /// 8 | public class XSEmptyElement 9 | : XSElement 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The represented by the . 16 | /// 17 | /// 18 | /// The , within the source text, spanned by the node. 19 | /// 20 | /// 21 | /// The range, within the source text, spanned by the element's name. 22 | /// 23 | /// 24 | /// The range, within the source text, spanned by the element's attributes. 25 | /// 26 | /// 27 | /// The 's parent element (if any). 28 | /// 29 | public XSEmptyElement(XmlEmptyElementSyntax emptyElement, Range range, Range nameRange, Range attributesRange, XSElement parent) 30 | : base(emptyElement, range, nameRange, attributesRange, parent) 31 | { 32 | } 33 | 34 | /// 35 | /// The represented by the . 36 | /// 37 | public new XmlEmptyElementSyntax ElementNode => (XmlEmptyElementSyntax)SyntaxNode; 38 | 39 | /// 40 | /// The kind of XML node represented by the . 41 | /// 42 | public override XSNodeKind Kind => XSNodeKind.Element; 43 | 44 | /// 45 | /// Does the represent valid XML? 46 | /// 47 | public override bool IsValid => true; 48 | 49 | /// 50 | /// Does the have any content (besides attributes)? 51 | /// 52 | public override bool HasContent => false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSInvalidAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Represents an invalid XML attribute. 7 | /// 8 | public class XSInvalidAttribute 9 | : XSAttribute 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The represented by the . 16 | /// 17 | /// 18 | /// The element that contains the attribute. 19 | /// 20 | /// 21 | /// The , within the source text, spanned by the attribute. 22 | /// 23 | /// 24 | /// The , within the source text, spanned by the attribute's name. 25 | /// 26 | /// 27 | /// The , within the source text, spanned by the attribute's value. 28 | /// 29 | public XSInvalidAttribute(XmlAttributeSyntax attribute, XSElement element, Range range, Range nameRange, Range valueRange) 30 | : base(attribute, element, range, nameRange, valueRange) 31 | { 32 | } 33 | 34 | /// 35 | /// Does the represent valid XML? 36 | /// 37 | public override bool IsValid => false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSInvalidElement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Represents a invalid XML element (with or without content). 7 | /// 8 | public class XSInvalidElement 9 | : XSElement 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The represented by the . 16 | /// 17 | /// 18 | /// The , within the source text, spanned by the element. 19 | /// 20 | /// 21 | /// The range, within the source text, spanned by the element's name. 22 | /// 23 | /// 24 | /// The range, within the source text, spanned by the element's attributes. 25 | /// 26 | /// 27 | /// The 's parent element (if any). 28 | /// 29 | /// 30 | /// Does the have any content (besides attributes)? 31 | /// 32 | public XSInvalidElement(XmlElementSyntaxBase element, Range range, Range nameRange, Range attributesRange, XSElement parent, bool hasContent) 33 | : base(element, range, nameRange, attributesRange, parent) 34 | { 35 | if (parent == null) 36 | System.Diagnostics.Debugger.Break(); 37 | 38 | HasContent = hasContent; 39 | } 40 | 41 | /// 42 | /// Does the represent valid XML? 43 | /// 44 | public override bool IsValid => false; 45 | 46 | /// 47 | /// Does the have any content (besides attributes)? 48 | /// 49 | public override bool HasContent { get; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSNode.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | using System; 3 | 4 | using MLXML = Microsoft.Language.Xml; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 7 | { 8 | /// 9 | /// Represents an XML node in the semantic model. 10 | /// 11 | public abstract class XSNode 12 | { 13 | // TODO: Consider storing TextPositions here to allow XSNode and friends to calculate Positions and Ranges as-needed. 14 | 15 | /// 16 | /// Create a new . 17 | /// 18 | /// 19 | /// The , within the source text, spanned by the node. 20 | /// 21 | protected XSNode(Range range) 22 | { 23 | Range = range; 24 | } 25 | 26 | /// 27 | /// The , within the source text, spanned by the node. 28 | /// 29 | public Range Range { get; } 30 | 31 | /// 32 | /// The node's path within the XML. 33 | /// 34 | public abstract XSPath Path { get; } 35 | 36 | /// 37 | /// The node's starting position. 38 | /// 39 | public Position Start => Range.Start; 40 | 41 | /// 42 | /// The node's ending position. 43 | /// 44 | public Position End => Range.End; 45 | 46 | /// 47 | /// The node's next sibling node (if any). 48 | /// 49 | public XSNode NextSibling { get; internal set; } 50 | 51 | /// 52 | /// The node's previous sibling node (if any). 53 | /// 54 | public XSNode PreviousSibling { get; internal set; } 55 | 56 | /// 57 | /// The kind of XML node represented by the . 58 | /// 59 | public abstract XSNodeKind Kind { get; } 60 | 61 | /// 62 | /// The node name. 63 | /// 64 | public abstract string Name { get; } 65 | 66 | /// 67 | /// Does the represent valid XML? 68 | /// 69 | public abstract bool IsValid { get; } 70 | } 71 | 72 | /// 73 | /// Represents an XML node in the semantic model with a known type of corresponding . 74 | /// 75 | /// 76 | /// The type of represented by the . 77 | /// 78 | public abstract class XSNode 79 | : XSNode 80 | where TSyntax : SyntaxNode 81 | { 82 | /// 83 | /// Create a new . 84 | /// 85 | /// 86 | /// The represented by the . 87 | /// 88 | /// 89 | /// The , within the source text, spanned by the node. 90 | /// 91 | protected XSNode(TSyntax syntaxNode, Range range) 92 | : base(range) 93 | { 94 | if (syntaxNode == null) 95 | throw new ArgumentNullException(nameof(syntaxNode)); 96 | 97 | SyntaxNode = syntaxNode; 98 | } 99 | 100 | /// 101 | /// The underlying represented by the . 102 | /// 103 | protected TSyntax SyntaxNode { get; } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Extension methods for working with s. 7 | /// 8 | public static class XSNodeExtensions 9 | { 10 | /// 11 | /// Determine whether the 's path starts with the specified . 12 | /// 13 | /// 14 | /// The . 15 | /// 16 | /// 17 | /// The . 18 | /// 19 | /// 20 | /// true, if starts with ; otherwise, false. 21 | /// 22 | public static bool PathStartsWith(this XSNode node, XSPath path) 23 | { 24 | if (node == null) 25 | throw new ArgumentNullException(nameof(node)); 26 | 27 | if (path == null) 28 | throw new ArgumentNullException(nameof(path)); 29 | 30 | return node.Path.StartsWith(path); 31 | } 32 | 33 | /// 34 | /// Determine whether the 's path ends with the specified . 35 | /// 36 | /// 37 | /// The . 38 | /// 39 | /// 40 | /// The . 41 | /// 42 | /// 43 | /// true, if ends with ; otherwise, false. 44 | /// 45 | public static bool PathEndsWith(this XSNode node, XSPath path) 46 | { 47 | if (node == null) 48 | throw new ArgumentNullException(nameof(node)); 49 | 50 | if (path == null) 51 | throw new ArgumentNullException(nameof(path)); 52 | 53 | return node.Path.EndsWith(path); 54 | } 55 | 56 | /// 57 | /// Determine whether the 's parent path is equal to the specified . 58 | /// 59 | /// 60 | /// The . 61 | /// 62 | /// 63 | /// The . 64 | /// 65 | /// 66 | /// true, if the node's path is equal to ; otherwise, false. 67 | /// 68 | public static bool HasParentPath(this XSNode node, XSPath parentPath) 69 | { 70 | if (node == null) 71 | throw new ArgumentNullException(nameof(node)); 72 | 73 | if (parentPath == null) 74 | throw new ArgumentNullException(nameof(parentPath)); 75 | 76 | XSPath nodeParentPath = node.Path.Parent; 77 | if (nodeParentPath == null) 78 | return false; 79 | 80 | // The common use case for this is checking if an element or attribute matches a relative parent path (e.g. match both Project/ItemGroup and Project/Target/ItemGroup). 81 | if (parentPath.IsRelative) 82 | return nodeParentPath.EndsWith(parentPath); 83 | 84 | return node.Path.IsChildOf(parentPath); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSNodeKind.cs: -------------------------------------------------------------------------------- 1 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 2 | { 3 | /// 4 | /// Well-known types of XML nodes in the semantic model. 5 | /// 6 | public enum XSNodeKind 7 | { 8 | /// 9 | /// An unknown node type. 10 | /// 11 | /// 12 | /// Used to detect uninitialized values; do not use directly. 13 | /// 14 | Unknown = 0, 15 | 16 | /// 17 | /// An XML element. 18 | /// 19 | Element = 1, 20 | 21 | /// 22 | /// An XML attribute. 23 | /// 24 | Attribute = 2, 25 | 26 | /// 27 | /// Text content. 28 | /// 29 | Text = 3, 30 | 31 | /// 32 | /// Non-significant whitespace (the syntax model calls this whitespace trivia). 33 | /// 34 | Whitespace = 4 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XSWhitespace.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Represents non-significant whitespace (the syntax model refers to this as whitespace trivia). 7 | /// 8 | public class XSWhitespace 9 | : XSNode 10 | { 11 | /// 12 | /// The whitespace's path within the XML. 13 | /// 14 | readonly XSPath _path; 15 | 16 | /// 17 | /// Create new . 18 | /// 19 | /// 20 | /// The , within the source text, spanned by the whitespace. 21 | /// 22 | /// 23 | /// The that contains the whitespace. 24 | /// 25 | public XSWhitespace(Range range, XSElement parent) 26 | : base(range) 27 | { 28 | if (parent == null) 29 | throw new ArgumentNullException(nameof(parent)); 30 | 31 | ParentElement = parent; 32 | 33 | XSPath parentPath = parent?.Path ?? XSPath.Root; 34 | _path = parentPath + Name; 35 | } 36 | 37 | /// 38 | /// The kind of . 39 | /// 40 | public override XSNodeKind Kind => XSNodeKind.Whitespace; 41 | 42 | /// 43 | /// The node name. 44 | /// 45 | public override string Name => "#whitespace"; 46 | 47 | /// 48 | /// The whitespace's path within the XML. 49 | /// 50 | public override XSPath Path => _path; 51 | 52 | /// 53 | /// The that contains the whitespace. 54 | /// 55 | public XSElement ParentElement { get; } 56 | 57 | /// 58 | /// Does the represent valid XML? 59 | /// 60 | public override bool IsValid => true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XmlExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | /// 7 | /// Helper methods for working with types from System.Xml. 8 | /// 9 | public static class XmlExceptionExtensions 10 | { 11 | /// 12 | /// Get the represented by the . 13 | /// 14 | /// 15 | /// The . 16 | /// 17 | /// 18 | /// The XML locator API (if available). 19 | /// 20 | /// 21 | /// The . 22 | /// 23 | public static Range GetRange(this XmlException invalidXml, XmlLocator xmlLocator) 24 | { 25 | if (invalidXml == null) 26 | throw new ArgumentNullException(nameof(invalidXml)); 27 | 28 | var startPosition = new Position( 29 | invalidXml.LineNumber, 30 | invalidXml.LinePosition 31 | ); 32 | 33 | // Attempt to use the range of the actual XML that the exception refers to. 34 | XmlLocation location = xmlLocator?.Inspect(startPosition); 35 | if (location != null) 36 | return location.Node.Range; 37 | 38 | // Otherwise, just use the start position. 39 | return startPosition.ToEmptyRange(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XmlLocationFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 4 | { 5 | /// 6 | /// Flags describing a location in XML. 7 | /// 8 | [Flags] 9 | public enum XmlLocationFlags 10 | { 11 | /// 12 | /// No flags. 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// Position is on an element. 18 | /// 19 | Element = 1, 20 | 21 | /// 22 | /// Position is on the opening tag of an element. 23 | /// 24 | OpeningTag = 2, 25 | 26 | /// 27 | /// Position is on the closing tag of an element. 28 | /// 29 | ClosingTag = 4, 30 | 31 | /// 32 | /// Position is on an attribute. 33 | /// 34 | Attribute = 8, 35 | 36 | /// 37 | /// Position is within an element's attributes range (but not on a specific attribute). 38 | /// 39 | Attributes = 16, 40 | 41 | /// 42 | /// Position is on a name. 43 | /// 44 | Name = 32, 45 | 46 | /// 47 | /// Position is on element content / attribute value. 48 | /// 49 | Value = 64, 50 | 51 | /// 52 | /// Position is on text. 53 | /// 54 | Text = 128, 55 | 56 | /// 57 | /// Position is on whitespace. 58 | /// 59 | Whitespace = 256, 60 | 61 | /// 62 | /// Element or attribute has no content. 63 | /// 64 | Empty = 1024, 65 | 66 | /// 67 | /// Node at location does not represent valid XML. 68 | /// 69 | Invalid = 2048 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/LanguageServer.SemanticModel.Xml/XmlModelConversions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | using System; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.SemanticModel 5 | { 6 | using Utilities; 7 | 8 | /// 9 | /// Extension methods for converting models between native and third-party representations. 10 | /// 11 | public static class XmlModelConversions 12 | { 13 | /// 14 | /// Convert the to its native equivalent. 15 | /// 16 | /// 17 | /// The to convert. 18 | /// 19 | /// 20 | /// The textual position lookup used to map absolute positions to lines and columns. 21 | /// 22 | /// 23 | /// The equivalent . 24 | /// 25 | public static Range ToNative(this TextSpan span, TextPositions textPositions) 26 | { 27 | if (textPositions == null) 28 | throw new ArgumentNullException(nameof(textPositions)); 29 | 30 | Position startPosition = textPositions.GetPosition(span.Start); 31 | Position endPosition = textPositions.GetPosition(span.End); 32 | if (endPosition.ColumnNumber == 0) 33 | throw new InvalidOperationException("Should not happen anymore"); 34 | 35 | return new Range(startPosition, endPosition); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/LanguageServer/LanguageServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | MSBuildProjectTools.LanguageServer.Host 5 | MSBuildProjectTools.LanguageServer.Host 6 | 7 | 8 | LatestMajor 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 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/EvaluationParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild evaluation expressions. 9 | /// 10 | public class EvaluationParserTests 11 | { 12 | /// 13 | /// Verify that the Evaluation parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected symbol name. 20 | /// 21 | [InlineData("$(Foo)", "Foo")] 22 | [InlineData("$( Foo )", "Foo")] 23 | [InlineData("$( Foo)", "Foo")] 24 | [InlineData("$(Foo )", "Foo")] 25 | [Theory(DisplayName = "Evaluation parser succeeds with symbol ")] 26 | public void Parse_Symbol_Success(string input, string expectedSymbolName) 27 | { 28 | AssertParser.SucceedsWith(Parsers.Evaluation, input, actualEvaluation => 29 | { 30 | var child = Assert.Single(actualEvaluation.Children); 31 | 32 | Symbol actualSymbol = Assert.IsType(child); 33 | Assert.Equal(expectedSymbolName, actualSymbol.Name); 34 | }); 35 | } 36 | 37 | /// 38 | /// Verify that the Evaluation parser can successfully parse the specified input. 39 | /// 40 | /// 41 | /// The source text to parse. 42 | /// 43 | /// 44 | /// The expected function name. 45 | /// 46 | [InlineData("$( Foo() )", "Foo")] 47 | [InlineData("$( Foo('Bar') )", "Foo")] 48 | [InlineData("$( Foo('Bar', 'Bonk') )", "Foo")] 49 | [InlineData("$(Foo.Bar())", "Bar")] 50 | [InlineData("$(Foo.Bar('Bonk', 'Diddly'))", "Bar")] 51 | [InlineData("$(Foo.Bar('Baz'))", "Bar")] 52 | [InlineData("$([Foo.Bar]::Baz('Bonk'))", "Baz")] 53 | [Theory(DisplayName = "Evaluation parser succeeds with function-call ")] 54 | public void Parse_FunctionCall_Success(string input, string expectedFunctionName) 55 | { 56 | AssertParser.SucceedsWith(Parsers.Evaluation, input, actualEvaluation => 57 | { 58 | var child = Assert.Single(actualEvaluation.Children); 59 | 60 | FunctionCall actualFunctionCall = Assert.IsType(child); 61 | Assert.Equal(expectedFunctionName, actualFunctionCall.Name); 62 | }); 63 | } 64 | 65 | /// 66 | /// Verify that the EvaluationExpression parser cannot successfully parse the specified input. 67 | /// 68 | /// 69 | /// The source text to parse. 70 | /// 71 | [InlineData("$(1Foo)")] 72 | [InlineData("$(Foo.Bar)")] 73 | [Theory(DisplayName = "Evaluation parser fails ")] 74 | public void Parse_Symbol_Failure(string input) 75 | { 76 | AssertParser.Fails(Parsers.Evaluation, input); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/GroupedExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild grouped expressions. 9 | /// 10 | public class GroupedExpressionTests 11 | { 12 | /// 13 | /// Verify that the GroupedExpression parser can successfully parse a logical unary expression composed of a comparison between quoted strings. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected kind of the root expression. 20 | /// 21 | [InlineData("(! ABC)", ExpressionKind.Logical)] 22 | [InlineData("((! ABC))", ExpressionKind.Logical)] 23 | [InlineData("('ABC' != 'DEF')", ExpressionKind.Compare)] 24 | [InlineData("(('ABC' != 'DEF'))", ExpressionKind.Compare)] 25 | [InlineData("(! ('ABC' != 'DEF'))", ExpressionKind.Logical)] 26 | [InlineData("(! (('ABC' != 'DEF')))", ExpressionKind.Logical)] 27 | [Theory(DisplayName = "GroupedExpression parser succeeds ")] 28 | public void Parse_Success(string input, ExpressionKind expectedRootExpressionKind) 29 | { 30 | AssertParser.SucceedsWith(Parsers.GroupedExpression, input, actualExpression => 31 | { 32 | Assert.Equal(expectedRootExpressionKind, actualExpression.Kind); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/ItemGroupParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild item group expressions. 9 | /// 10 | public class ItemGroupParserTests 11 | { 12 | /// 13 | /// Verify that the ItemGroup parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected symbol name. 20 | /// 21 | [InlineData("@()", "" )] 22 | [InlineData("@(Foo)", "Foo")] 23 | [InlineData("@( Foo )", "Foo")] 24 | [InlineData("@( Foo)", "Foo")] 25 | [InlineData("@(Foo )", "Foo")] 26 | [Theory(DisplayName = "ItemGroup parser succeeds ")] 27 | public void Parse_Success(string input, string expectedItemGroupName) 28 | { 29 | AssertParser.SucceedsWith(Parsers.ItemGroup, input, actualItemGroup => 30 | { 31 | Assert.Equal(expectedItemGroupName, actualItemGroup.Name); 32 | }); 33 | } 34 | 35 | /// 36 | /// Verify that the ItemGroup parser cannot successfully parse the specified input. 37 | /// 38 | /// 39 | /// The source text to parse. 40 | /// 41 | [InlineData("@(1Foo)" )] 42 | [InlineData("@(Foo.Bar)")] 43 | [Theory(DisplayName = "ItemGroup parser fails ")] 44 | public void Parse_Failure(string input) 45 | { 46 | AssertParser.Fails(Parsers.ItemGroup, input); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/ItemGroupTransformParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild item group expressions. 9 | /// 10 | public class ItemGroupTransformParserTests 11 | { 12 | /// 13 | /// Verify that the ItemGroupTransform parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected item group name. 20 | /// 21 | /// 22 | /// Expect the item group transform to have an expression body? 23 | /// 24 | /// 25 | /// Expect the item group transform to have a separator. 26 | /// 27 | [InlineData("@(Foo->)", "Foo", false, false)] 28 | [InlineData("@(Foo->'')", "Foo", true, false )] 29 | [InlineData("@(Foo ->'')", "Foo", true, false )] 30 | [InlineData("@(Foo -> '')", "Foo", true, false )] 31 | [InlineData("@(Foo->'','')", "Foo", true, true )] 32 | [InlineData("@(Foo->'', '')", "Foo", true, true )] 33 | [InlineData("@(Foo->'$(Bar)')", "Foo", true, false )] 34 | [InlineData("@(Foo->'%(Bar)')", "Foo", true, false )] 35 | [Theory(DisplayName = "ItemGroupTransform parser succeeds ")] 36 | public void Parse_Success(string input, string expectedItemType, bool expectBody, bool expectSeparator) 37 | { 38 | AssertParser.SucceedsWith(Parsers.ItemGroupTransform, input, actualItemGroupTransform => 39 | { 40 | Assert.Equal(expectedItemType, actualItemGroupTransform.Name); 41 | 42 | if (expectBody) 43 | Assert.True(actualItemGroupTransform.HasBody, "HasBody"); 44 | else 45 | Assert.False(actualItemGroupTransform.HasBody, "HasBody"); 46 | 47 | if (expectSeparator) 48 | Assert.True(actualItemGroupTransform.HasSeparator, "HasSeparator"); 49 | else 50 | Assert.False(actualItemGroupTransform.HasSeparator, "HasSeparator"); 51 | }); 52 | } 53 | 54 | /// 55 | /// Verify that the ItemGroupTransform parser cannot successfully parse the specified input. 56 | /// 57 | /// 58 | /// The source text to parse. 59 | /// 60 | [InlineData("@(Foo)" )] 61 | [InlineData("@(1Foo)" )] 62 | [InlineData("@(Foo.Bar)")] 63 | [Theory(DisplayName = "ItemGroupTransform parser fails ")] 64 | public void Parse_Failure(string input) 65 | { 66 | AssertParser.Fails(Parsers.ItemGroupTransform, input); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/ItemMetatadaParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | using Utilities; 7 | 8 | /// 9 | /// Tests for parsing of MSBuild item metadata expressions. 10 | /// 11 | public class ItemMetadataParserTests 12 | { 13 | /// 14 | /// Verify that the ItemMetadata parser can successfully parse an unqualified item metadata expression. 15 | /// 16 | /// 17 | /// The source text to parse. 18 | /// 19 | /// 20 | /// The expected metadata name. 21 | /// 22 | [InlineData("%()", "" )] 23 | [InlineData("%(Foo)", "Foo")] 24 | [InlineData("%( Foo )", "Foo")] 25 | [InlineData("%( Foo)", "Foo")] 26 | [InlineData("%(Foo )", "Foo")] 27 | [Theory(DisplayName = "ItemMetadata parser succeeds ")] 28 | public void Parse_Unqualified_Success(string input, string expectedMetadataName) 29 | { 30 | AssertParser.SucceedsWith(Parsers.ItemMetadata, input, actualItemMetadata => 31 | { 32 | actualItemMetadata.PostParse( 33 | new TextPositions(input) 34 | ); 35 | 36 | Assert.Equal(expectedMetadataName, actualItemMetadata.Name); 37 | }); 38 | } 39 | 40 | /// 41 | /// Verify that the ItemMetadata parser can successfully parse a qualified item metadata expression. 42 | /// 43 | /// 44 | /// The source text to parse. 45 | /// 46 | /// 47 | /// The expected item type. 48 | /// 49 | /// 50 | /// The expected metadata name. 51 | /// 52 | [InlineData("%(Foo.Bar)", "Foo", "Bar")] 53 | [InlineData("%( Foo.Bar )", "Foo", "Bar")] 54 | [InlineData("%( Foo .Bar)", "Foo", "Bar")] 55 | [InlineData("%(Foo.Bar )", "Foo", "Bar")] 56 | [InlineData("%(Foo.)", "Foo", "" )] 57 | [InlineData("%(Foo. )", "Foo", " " )] 58 | [Theory(DisplayName = "ItemMetadata parser succeeds ")] 59 | public void Parse_Qualified_Success(string input, string expectedItemType, string expectedMetadataName) 60 | { 61 | AssertParser.SucceedsWith(Parsers.ItemMetadata, input, actualItemMetadata => 62 | { 63 | Assert.Equal(expectedItemType, actualItemMetadata.ItemType); 64 | Assert.Equal(expectedMetadataName, actualItemMetadata.Name); 65 | }); 66 | } 67 | 68 | /// 69 | /// Verify that the ItemMetadata parser cannot successfully parse the specified input. 70 | /// 71 | /// 72 | /// The source text to parse. 73 | /// 74 | [InlineData("%(1Foo)")] 75 | [Theory(DisplayName = "ItemMetadata parser fails ")] 76 | public void Parse_Failure(string input) 77 | { 78 | AssertParser.Fails(Parsers.ItemMetadata, input); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/LocatorExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Language.Xml; 2 | using System; 3 | using System.IO; 4 | using Xunit; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 7 | { 8 | using SemanticModel; 9 | using SemanticModel.MSBuildExpressions; 10 | using Utilities; 11 | 12 | /// 13 | /// Tests for 's IsExpression and friends. 14 | /// 15 | public class LocatorExpressionTests 16 | { 17 | /// 18 | /// The directory for test files. 19 | /// 20 | static readonly DirectoryInfo TestDirectory = new DirectoryInfo(Path.GetDirectoryName( 21 | typeof(XmlLocatorTests).Assembly.Location 22 | )); 23 | 24 | /// 25 | /// Verify that the target line and column are on an expression. 26 | /// 27 | /// 28 | /// The name of the test file, without the extension. 29 | /// 30 | /// 31 | /// The target line. 32 | /// 33 | /// 34 | /// The target column. 35 | /// 36 | /// 37 | /// The expected kind of expression. 38 | /// 39 | [Theory(DisplayName = "On expression ")] 40 | [InlineData("Test4", 3, 25, ExpressionKind.Evaluate )] 41 | [InlineData("Test4", 4, 25, ExpressionKind.ItemMetadata)] 42 | [InlineData("Test4", 5, 30, ExpressionKind.Symbol )] 43 | [InlineData("Test4", 5, 46, ExpressionKind.ItemMetadata)] 44 | public void IsExpression_Success(string testFileName, int line, int column, ExpressionKind expectedExpressionKind) 45 | { 46 | var testPosition = new Position(line, column); 47 | 48 | string testXml = LoadTestFile("TestFiles", testFileName + ".xml"); 49 | var positions = new TextPositions(testXml); 50 | XmlDocumentSyntax document = Parser.ParseText(testXml); 51 | 52 | var locator = new XmlLocator(document, positions); 53 | XmlLocation location = locator.Inspect(testPosition); 54 | Assert.NotNull(location); 55 | 56 | Assert.True( 57 | location.IsExpression(out ExpressionNode actualExpression, out Range actualExpressionRange), 58 | "IsExpression" 59 | ); 60 | Assert.NotNull(actualExpression); 61 | 62 | Assert.Equal(expectedExpressionKind, actualExpression.Kind); 63 | } 64 | 65 | /// 66 | /// Load a test file. 67 | /// 68 | /// 69 | /// The file's relative path segments. 70 | /// 71 | /// 72 | /// The file content, as a string. 73 | /// 74 | static string LoadTestFile(params string[] relativePathSegments) 75 | { 76 | if (relativePathSegments == null) 77 | throw new ArgumentNullException(nameof(relativePathSegments)); 78 | 79 | return File.ReadAllText( 80 | Path.Combine( 81 | TestDirectory.FullName, 82 | Path.Combine(relativePathSegments) 83 | ) 84 | ); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/QuotedStringLiteralParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild quoted-string literal expressions. 9 | /// 10 | public class QuotedStringLiteralParserTests 11 | { 12 | /// 13 | /// Verify that the QuotedStringLiteralExpression parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected string content. 20 | /// 21 | [InlineData("''", "" )] 22 | [InlineData("'ABC'", "ABC" )] 23 | [InlineData("'ABC '", "ABC " )] 24 | [InlineData("' ABC'", " ABC" )] 25 | [InlineData("' ABC '", " ABC ")] 26 | [Theory(DisplayName = "QuotedStringLiteral parser succeeds ")] 27 | public void Parse_Success(string input, string expectedContent) 28 | { 29 | AssertParser.SucceedsWith(Parsers.QuotedStringLiteral, input, actualQuotedStringLiteral => 30 | { 31 | Assert.Equal(expectedContent, actualQuotedStringLiteral.Content); 32 | }); 33 | } 34 | 35 | /// 36 | /// Verify that the QuotedStringLiteral parser cannot successfully parse the specified input. 37 | /// 38 | /// 39 | /// The source text to parse. 40 | /// 41 | [InlineData("ABC" )] 42 | [InlineData("ABC " )] 43 | [InlineData(" ABC" )] 44 | [InlineData(" ABC ")] 45 | [Theory(DisplayName = "QuotedStringLiteral parser fails for unquoted string ")] 46 | public void Parse_Unquoted_Failure(string input) 47 | { 48 | AssertParser.Fails(Parsers.QuotedStringLiteral, input); 49 | } 50 | 51 | /// 52 | /// Verify that the QuotedStringLiteral parser cannot successfully parse the specified input. 53 | /// 54 | /// 55 | /// The source text to parse. 56 | /// 57 | [InlineData("'ABC" )] 58 | [InlineData("AB'C " )] 59 | [InlineData(" ABC'" )] 60 | [InlineData(" ABC' ")] 61 | [Theory(DisplayName = "QuotedStringLiteral parser fails for string without closing quote ")] 62 | public void Parse_Without_ClosingQuote_Failure(string input) 63 | { 64 | AssertParser.Fails(Parsers.QuotedStringLiteral, input); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/TokenParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild tokens. 9 | /// 10 | public class TokenParserTests 11 | { 12 | /// 13 | /// Verify that the Identifier parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected identifier name. 20 | /// 21 | [InlineData("F", "F" )] 22 | [InlineData("Foo", "Foo")] 23 | [Theory(DisplayName = "Identifier token parser succeeds ")] 24 | public void Parse_Success(string input, string expectedIdentifierName) 25 | { 26 | AssertParser.SucceedsWith(Tokens.Identifier, input, actualToken => 27 | { 28 | Assert.Equal(expectedIdentifierName, actualToken); 29 | }); 30 | } 31 | 32 | /// 33 | /// Verify that the Identifier parser cannot successfully parse the specified input. 34 | /// 35 | /// 36 | /// The source text to parse. 37 | /// 38 | [InlineData("1" )] 39 | [InlineData("1Foo")] 40 | [Theory(DisplayName = "Identifier token parser fails ")] 41 | public void Parse_Failure(string input) 42 | { 43 | AssertParser.Fails(Tokens.Identifier, input); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/ExpressionTests/TypeRefParserTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests.ExpressionTests 4 | { 5 | using SemanticModel.MSBuildExpressions; 6 | 7 | /// 8 | /// Tests for parsing of MSBuild type-reference expressions. 9 | /// 10 | public class TypeRefParserTests 11 | { 12 | /// 13 | /// Verify that the TypeRef parser can successfully parse the specified input. 14 | /// 15 | /// 16 | /// The source text to parse. 17 | /// 18 | /// 19 | /// The expected type name. 20 | /// 21 | /// 22 | /// The expected type namespace. 23 | /// 24 | [InlineData("[Foo]", "Foo", "" )] 25 | [InlineData("[Foo.Bar]", "Bar", "Foo")] 26 | [Theory(DisplayName = "TypeRef parser succeeds with symbol ")] 27 | public void Parse_Success(string input, string expectedTypeName, string expectedTypeNamespace) 28 | { 29 | AssertParser.SucceedsWith(Parsers.TypeRef, input, actualTypeRef => 30 | { 31 | Assert.Equal(expectedTypeName, actualTypeRef.Name); 32 | Assert.Equal(expectedTypeNamespace, actualTypeRef.Namespace); 33 | }); 34 | } 35 | 36 | /// 37 | /// Verify that the TypeRefExpression parser cannot successfully parse the specified input. 38 | /// 39 | /// 40 | /// The source text to parse. 41 | /// 42 | [InlineData("[1Foo]" )] 43 | [InlineData("[Foo Bar]")] 44 | [Theory(DisplayName = "TypeRef parser fails ")] 45 | public void Parse_Symbol_Failure(string input) 46 | { 47 | AssertParser.Fails(Parsers.TypeRef, input); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/LanguageServer.Engine.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | MSBuildProjectTools.LanguageServer.Tests 5 | MSBuildProjectTools.LanguageServer.Engine.Tests 6 | x64 7 | 8 | 9 | 10 | 1701;1702;1705;IDE0016 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/Logging/SerilogTestOutputExtensions.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Configuration; 3 | using Serilog.Core; 4 | using System; 5 | using Xunit.Abstractions; 6 | 7 | namespace MSBuildProjectTools.LanguageServer.Tests.Logging 8 | { 9 | /// 10 | /// Extension methods for configuring Serilog. 11 | /// 12 | public static class SerilogTestOutputExtensions 13 | { 14 | /// 15 | /// Write log events to Xunit test output. 16 | /// 17 | /// 18 | /// The logger sink configuration. 19 | /// 20 | /// 21 | /// The test output to which events will be logged. 22 | /// 23 | /// 24 | /// An optional to control logging. 25 | /// 26 | /// 27 | /// The logger configuration. 28 | /// 29 | public static LoggerConfiguration TestOutput(this LoggerSinkConfiguration loggerSinkConfiguration, ITestOutputHelper testOutput, LoggingLevelSwitch levelSwitch = null) 30 | { 31 | if (loggerSinkConfiguration == null) 32 | throw new ArgumentNullException(nameof(loggerSinkConfiguration)); 33 | 34 | if (testOutput == null) 35 | throw new ArgumentNullException(nameof(testOutput)); 36 | 37 | return loggerSinkConfiguration.Sink( 38 | new TestOutputLoggingSink(testOutput, 39 | levelSwitch: levelSwitch ?? new LoggingLevelSwitch() 40 | ) 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/Logging/TestOutputSink.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | using System; 3 | using Serilog.Events; 4 | using Xunit.Abstractions; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.Tests.Logging 7 | { 8 | /// 9 | /// A Serilog logging sink that sends log events to the language server logging facility. 10 | /// 11 | public class TestOutputLoggingSink 12 | : ILogEventSink 13 | { 14 | /// 15 | /// The language server to which events will be logged. 16 | /// 17 | readonly ITestOutputHelper _testOutput; 18 | 19 | /// 20 | /// The that controls logging. 21 | /// 22 | readonly LoggingLevelSwitch _levelSwitch; 23 | 24 | /// 25 | /// Create a new language-server event sink. 26 | /// 27 | /// 28 | /// The language server to which events will be logged. 29 | /// 30 | /// 31 | /// The that controls logging. 32 | /// 33 | public TestOutputLoggingSink(ITestOutputHelper testOutput, LoggingLevelSwitch levelSwitch) 34 | { 35 | if (testOutput == null) 36 | throw new ArgumentNullException(nameof(testOutput)); 37 | 38 | if (levelSwitch == null) 39 | throw new ArgumentNullException(nameof(levelSwitch)); 40 | 41 | _testOutput = testOutput; 42 | _levelSwitch = levelSwitch; 43 | } 44 | 45 | /// 46 | /// Emit a log event. 47 | /// 48 | /// 49 | /// The log event information. 50 | /// 51 | public void Emit(LogEvent logEvent) 52 | { 53 | if (logEvent.Level < _levelSwitch.MinimumLevel) 54 | return; 55 | 56 | string message = logEvent.RenderMessage(); 57 | if (logEvent.Exception != null) 58 | message += "\n" + logEvent.Exception.ToString(); 59 | 60 | _testOutput.WriteLine(message); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/MSBuildEngineFixture.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace MSBuildProjectTools.LanguageServer.Tests 4 | { 5 | using Utilities; 6 | 7 | /// 8 | /// An xUnit collection fixture that ensures the MSBuild Locator API is called to discover and use the latest version of the MSBuild engine before any tests are run that depend on it. 9 | /// 10 | public sealed class MSBuildEngineFixture 11 | { 12 | public const string CollectionName = "MSBuild Engine"; 13 | 14 | /// 15 | /// Create a new . 16 | /// 17 | public MSBuildEngineFixture() 18 | { 19 | MSBuildHelper.DiscoverMSBuildEngine(); 20 | } 21 | } 22 | 23 | /// 24 | /// The collection-fixture binding for . 25 | /// 26 | [CollectionDefinition(MSBuildEngineFixture.CollectionName)] 27 | public sealed class MSBuildEngineFixtureCollection 28 | : ICollectionFixture 29 | { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/RangeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.Tests 7 | { 8 | /// 9 | /// Tests for . 10 | /// 11 | public class RangeTests 12 | { 13 | /// 14 | /// Built-in comparison for s should result in them being sorted by start position and then end position. 15 | /// 16 | /// 17 | /// The ranges under test. 18 | /// 19 | [MemberData(nameof(TestRanges))] 20 | [Theory(DisplayName = "Expect ranges to sort by start position, then end position ")] 21 | public void SortByStartThenEnd(Range[] ranges) 22 | { 23 | Range[] expected = ranges 24 | .OrderBy(range => range.Start) 25 | .ThenBy(range => range.End) 26 | .ToArray(); 27 | 28 | Array.Sort(ranges); 29 | 30 | Assert.Equal(expected, ranges); 31 | } 32 | 33 | /// 34 | /// Test data for tests that use ranges. 35 | /// 36 | public static IEnumerable TestRanges 37 | { 38 | get 39 | { 40 | static object[] DataRow(params Range[] ranges) => [ranges]; 41 | 42 | // Simulates node ranges after computing and appending nodes for whitespace. 43 | yield return DataRow( 44 | new Range(1, 1, 7, 12), 45 | new Range(2, 5, 5, 16), 46 | new Range(2, 15, 2, 34), 47 | new Range(3, 9, 3, 21), 48 | new Range(4, 9, 4, 21), 49 | new Range(6, 5, 6, 26), 50 | new Range(1, 11, 2, 5), 51 | new Range(5, 16, 6, 5), 52 | new Range(6, 26, 7, 1), 53 | new Range(2, 35, 3, 9), 54 | new Range(3, 21, 4, 9), 55 | new Range(4, 21, 5, 5) 56 | ); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/Stubs/StubDiagnosticPublisher.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace MSBuildProjectTools.LanguageServer.Tests.Stubs 7 | { 8 | using Diagnostics; 9 | 10 | /// 11 | /// A stub implementation of that captures published diagnostics. 12 | /// 13 | public class StubDiagnosticPublisher 14 | : IPublishDiagnostics 15 | { 16 | /// 17 | /// Create a new . 18 | /// 19 | public StubDiagnosticPublisher() 20 | { 21 | } 22 | 23 | /// 24 | /// Published diagnostics, keyed by document URI. 25 | /// 26 | public Dictionary Diagnostics { get; } = new Dictionary(); 27 | 28 | /// 29 | /// Publish the specified diagnostics. 30 | /// 31 | /// 32 | /// The URI of the document that the diagnostics apply to. 33 | /// 34 | /// 35 | /// A sequence of s to publish. 36 | /// 37 | public void Publish(Uri documentUri, IEnumerable diagnostics) 38 | { 39 | if (documentUri == null) 40 | throw new ArgumentNullException(nameof(documentUri)); 41 | 42 | if (diagnostics != null && diagnostics.Any()) 43 | Diagnostics[documentUri] = diagnostics.ToArray(); 44 | else 45 | Diagnostics.Remove(documentUri); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Context; 3 | using Serilog.Core; 4 | using System; 5 | using System.Reflection; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace MSBuildProjectTools.LanguageServer.Tests 10 | { 11 | using LanguageServer.Logging; 12 | using Logging; 13 | 14 | /// 15 | /// The base class for test suites. 16 | /// 17 | public abstract class TestBase 18 | { 19 | /// 20 | /// An representing the log context for the current test. 21 | /// 22 | readonly IDisposable _logContext; 23 | 24 | /// 25 | /// Create a new test-suite. 26 | /// 27 | /// 28 | /// Output for the current test. 29 | /// 30 | protected TestBase(ITestOutputHelper testOutput) 31 | { 32 | if (testOutput == null) 33 | throw new ArgumentNullException(nameof(testOutput)); 34 | 35 | TestOutput = testOutput; 36 | 37 | // Redirect component logging to Serilog. 38 | Log = 39 | new LoggerConfiguration() 40 | .MinimumLevel.Verbose() 41 | .Enrich.WithCurrentActivityId() 42 | .Enrich.FromLogContext() 43 | .WriteTo.TestOutput(TestOutput, LogLevelSwitch) 44 | .CreateLogger(); 45 | 46 | // Ugly hack to get access to the current test. 47 | CurrentTest = 48 | (ITest)TestOutput.GetType() 49 | .GetField("test", BindingFlags.NonPublic | BindingFlags.Instance) 50 | .GetValue(TestOutput); 51 | 52 | Assert.True(CurrentTest != null, "Cannot retrieve current test from ITestOutputHelper."); 53 | 54 | _logContext = LogContext.PushProperty("TestName", CurrentTest.DisplayName); 55 | } 56 | 57 | /// 58 | /// Output for the current test. 59 | /// 60 | protected ITestOutputHelper TestOutput { get; } 61 | 62 | /// 63 | /// A representing the current test. 64 | /// 65 | protected ITest CurrentTest { get; } 66 | 67 | /// 68 | /// The Serilog logger for the current test. 69 | /// 70 | protected ILogger Log { get; } 71 | 72 | /// 73 | /// A switch to control the logging level for the current test. 74 | /// 75 | protected LoggingLevelSwitch LogLevelSwitch { get; } = new LoggingLevelSwitch(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid1.DoubleOpeningTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | < 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid1.EmptyOpeningTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <> 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid2.DoubleOpeningTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | MyPackage 5 | AnotherPackage 6 | false 7 | Hello 8 | 9 | 10 | 11 | 12 | 13 | < 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid2.EmptyOpeningTag.ChildOfRoot.xml: -------------------------------------------------------------------------------- 1 | 2 | <> 3 | 4 | 5 | netstandard1.3 6 | MyPackage 7 | AnotherPackage 8 | false 9 | Hello 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid2.EmptyOpeningTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | MyPackage 5 | AnotherPackage 6 | false 7 | Hello 8 | 9 | 10 | 11 | 12 | 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Invalid2.NoClosingTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | MyPackage 5 | AnotherPackage 6 | false 7 | Hello 8 | 9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Test1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Test2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | MyPackage 5 | AnotherPackage 6 | false 7 | Hello 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Test3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestFiles/Test4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace MSBuildProjectTools.LanguageServer.Tests 5 | { 6 | ///

7 | /// Helper functions for use in tests. 8 | /// 9 | static class TestHelper 10 | { 11 | /// 12 | /// Attempt to find a file in the target directory or one of its ancestors. 13 | /// 14 | /// 15 | /// A representing the target directory. 16 | /// 17 | /// 18 | /// The name of the file to find. 19 | /// 20 | /// 21 | /// A representing the file, if one was found; otherwise, null. 22 | /// 23 | public static FileInfo GetFileFromCurrentOrAncestor(this DirectoryInfo directory, string fileName) 24 | { 25 | if (directory == null) 26 | throw new ArgumentNullException(nameof(directory)); 27 | 28 | if (string.IsNullOrWhiteSpace(fileName)) 29 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(fileName)}.", nameof(fileName)); 30 | 31 | FileInfo file = null; 32 | var targetDirectory = new DirectoryInfo(directory.FullName); 33 | while (file == null) 34 | { 35 | file = new FileInfo( 36 | Path.Combine(targetDirectory.FullName, fileName) 37 | ); 38 | if (file.Exists) 39 | break; 40 | 41 | file = null; 42 | targetDirectory = targetDirectory.Parent; 43 | if (targetDirectory == null) 44 | break; 45 | } 46 | 47 | return file; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestProjects/Project1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.3 4 | MyPackage 5 | AnotherPackage 6 | false 7 | Hello 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/LanguageServer.Engine.Tests/TestProjects/RedefineProperty.SameFile.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | true 6 | 7 | 8 | --------------------------------------------------------------------------------