Skip to content

Commit b12f16f

Browse files
committed
Implemented a source generator for VSIX manifest properties.
1 parent b666724 commit b12f16f

22 files changed

+1323
-0
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ csharp_new_line_before_members_in_anonymous_types = true
105105
# IDE
106106
dotnet_diagnostic.IDE0058.severity = none
107107
dotnet_diagnostic.RS2008.severity = none # RS2008: Enable analyzer release tracking
108+
109+
# IDE0057: Use range operator
110+
csharp_style_prefer_range_operator = false
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.1.31911.260
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2E0ADDC4-4AA5-4D57-B309-DAF502E7555C}"
7+
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.VisualStudio.Generators", "src\Community.VisualStudio.SourceGenerators\Community.VisualStudio.SourceGenerators.csproj", "{3B743A03-8B3A-4DB3-8C20-D65DE25DF537}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B796B64-A789-44CD-8B82-8EFC6784FF71}"
11+
ProjectSection(SolutionItems) = preProject
12+
.editorconfig = .editorconfig
13+
Directory.Build.props = Directory.Build.props
14+
LICENSE = LICENSE
15+
README.md = README.md
16+
EndProjectSection
17+
EndProject
18+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{589EC942-90CD-4CBF-AEBF-1F21C7EEE3B0}"
19+
EndProject
20+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.VisualStudio.Generators.UnitTests", "test\Community.VisualStudio.SourceGenerators.UnitTests\Community.VisualStudio.SourceGenerators.UnitTests.csproj", "{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}"
21+
EndProject
22+
Global
23+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
24+
Debug|Any CPU = Debug|Any CPU
25+
Release|Any CPU = Release|Any CPU
26+
EndGlobalSection
27+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
28+
{3B743A03-8B3A-4DB3-8C20-D65DE25DF537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29+
{3B743A03-8B3A-4DB3-8C20-D65DE25DF537}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{3B743A03-8B3A-4DB3-8C20-D65DE25DF537}.Release|Any CPU.ActiveCfg = Release|Any CPU
31+
{3B743A03-8B3A-4DB3-8C20-D65DE25DF537}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F}.Release|Any CPU.Build.0 = Release|Any CPU
36+
EndGlobalSection
37+
GlobalSection(SolutionProperties) = preSolution
38+
HideSolutionNode = FALSE
39+
EndGlobalSection
40+
GlobalSection(NestedProjects) = preSolution
41+
{3B743A03-8B3A-4DB3-8C20-D65DE25DF537} = {2E0ADDC4-4AA5-4D57-B309-DAF502E7555C}
42+
{9D7B8767-CF76-4A59-A5A3-3F0B1053DE5F} = {589EC942-90CD-4CBF-AEBF-1F21C7EEE3B0}
43+
EndGlobalSection
44+
GlobalSection(ExtensibilityGlobals) = postSolution
45+
SolutionGuid = {10D68B3E-E09C-4ECE-ADD5-FE2B654434D8}
46+
EndGlobalSection
47+
EndGlobal

Directory.Build.props

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<LangVersion>10</LangVersion>
5+
<Nullable>enable</Nullable>
6+
<DebugSymbols>true</DebugSymbols>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<AssemblyAttribute Include="System.CLSCompliantAttribute">
13+
<_Parameter1>false</_Parameter1>
14+
</AssemblyAttribute>
15+
</ItemGroup>
16+
17+
</Project>

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 VSIX Community
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.

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Community source generators for Visual Studio extensions
2+
3+
Part of the [VSIX Community](https://github.com/VsixCommunity)
4+
5+
## Summary
6+
7+
This package contains [C# source generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) that generate code from `.vsixmanfiest` files.
8+
9+
These source generators are a replacement for the single-file generators from the [VsixSynchronizer](https://github.com/madskristensen/VsixSynchronizer) extension.
10+
11+
## VSIX Manifest Files
12+
13+
The source generator will create a class called `Vsix` with the following constants:
14+
15+
|Constant |Source |
16+
|-------------|----------------------------|
17+
|`Author` | `<Identity Publisher=""/>`|
18+
|`Description`| `<Description/>` |
19+
|`Id` | `<Identity Id=""/>` |
20+
|`Language` | `<Identity Language=""/>` |
21+
|`Name` | `<DisplayName/>` |
22+
|`Version` | `<Identity Version=""/>` |
23+
24+
#### Use a custom namespace
25+
26+
The `Vsix` class will be generated in the root namespace of the project. If you would like to generate the code into a different namespace, you can specify the namespace by defining the `Namespace` metadata for the `AdditionalFiles` item like this:
27+
28+
```xml
29+
<ItemGroup>
30+
<AdditionalFiles Update="source.extension.vsixmanifest">
31+
<Namespace>MyCustomNamespace</Namespace>
32+
</AdditionalFiles>
33+
</ItemGroup>
34+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Community.VisualStudio.SourceGenerators;
4+
5+
internal static class CommonDiagnostics
6+
{
7+
internal const string Category = "VSIX";
8+
9+
internal static readonly DiagnosticDescriptor NoNamespace = new(
10+
DiagnosticIds.CVSSG003_NoNamespace,
11+
new LocalizableResourceString(nameof(Resources.CVSSG003_Title), Resources.ResourceManager, typeof(Resources)),
12+
new LocalizableResourceString(nameof(Resources.CVSSG003_MessageFormat), Resources.ResourceManager, typeof(Resources)),
13+
Category,
14+
DiagnosticSeverity.Error,
15+
true
16+
);
17+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<PackageDescription>C# source generators for Visual Studio extension development.</PackageDescription>
9+
<PackageTags>VisualStudio, VSSDK, SDK</PackageTags>
10+
<DebugType>portable</DebugType>
11+
<DevelopmentDependency>true</DevelopmentDependency>
12+
<IncludeBuildOutput>false</IncludeBuildOutput>
13+
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);PackBuildOutputs</TargetsForTfmSpecificContentInPackage>
14+
<NoWarn>$(NoWarn);NU5128</NoWarn>
15+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
20+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<Compile Update="Resources.Designer.cs">
25+
<DesignTime>True</DesignTime>
26+
<AutoGen>True</AutoGen>
27+
<DependentUpon>Resources.resx</DependentUpon>
28+
</Compile>
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<EmbeddedResource Update="Resources.resx">
33+
<Generator>ResXFileCodeGenerator</Generator>
34+
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
35+
</EmbeddedResource>
36+
</ItemGroup>
37+
38+
<ItemGroup>
39+
<AssemblyAttribute Include="System.Resources.NeutralResourcesLanguageAttribute">
40+
<_Parameter1>en-US</_Parameter1>
41+
</AssemblyAttribute>
42+
</ItemGroup>
43+
44+
<ItemGroup>
45+
<None Update="NuGet\build\Community.VisualStudio.SourceGenerators.props" Pack="true" PackagePath="build\Community.VisualStudio.SourceGenerators.props" />
46+
</ItemGroup>
47+
48+
<Target Name="PackBuildOutputs" BeforeTargets="_GetPackageFiles" DependsOnTargets="SatelliteDllsProjectOutputGroup">
49+
<ItemGroup>
50+
<Content Include="$(TargetPath)" Pack="true" PackagePath="analyzers\cs\" />
51+
<Content Include="@(SatelliteDllsProjectOutputGroupOutput)" Pack="true" PackagePath="analyzers\cs\%(SatelliteDllsProjectOutputGroupOutput.Culture)\" />
52+
</ItemGroup>
53+
</Target>
54+
55+
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Community.VisualStudio.SourceGenerators;
2+
3+
internal static class DiagnosticIds
4+
{
5+
public const string CVSSG001_ManifestFileNotFound = "CVSSG001";
6+
public const string CVSSG002_InvalidManifestFile = "CVSSG002";
7+
public const string CVSSG003_NoNamespace = "CVSSG003";
8+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Community.VisualStudio.SourceGenerators;
4+
5+
internal static class AdditionalTextExtensions
6+
{
7+
public static bool TryGetNamespace(this AdditionalText manifestFile, GeneratorExecutionContext context, out string value)
8+
{
9+
// Check if a namespace was specified in the metadata for the additional file.
10+
if (context.AnalyzerConfigOptions.GetOptions(manifestFile).TryGetValue("build_metadata.AdditionalFiles.Namespace", out string? fileNamespace))
11+
{
12+
if (!string.IsNullOrEmpty(fileNamespace))
13+
{
14+
value = fileNamespace;
15+
return true;
16+
}
17+
}
18+
19+
// Fall back to using the root namespace from the project.
20+
if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.RootNamespace", out string? rootNamespace))
21+
{
22+
if (!string.IsNullOrEmpty(rootNamespace))
23+
{
24+
value = rootNamespace;
25+
return true;
26+
}
27+
}
28+
29+
value = "";
30+
return false;
31+
}
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Text;
2+
3+
namespace Community.VisualStudio.SourceGenerators;
4+
5+
public abstract class GeneratorBase
6+
{
7+
private protected static void WritePreamble(StringBuilder builder)
8+
{
9+
builder.AppendLine("// <auto-generated/>");
10+
builder.AppendLine();
11+
builder.AppendLine("#nullable enable");
12+
builder.AppendLine();
13+
}
14+
15+
private protected static void WriteClassStart(StringBuilder builder, string namespaceName, string summaryDocumentation, string className)
16+
{
17+
builder.AppendLine($"namespace {namespaceName}");
18+
builder.AppendLine("{");
19+
builder.AppendLine($" /// <summary>{summaryDocumentation}</summary>");
20+
builder.AppendLine($" internal sealed partial class {className}");
21+
builder.AppendLine(" {");
22+
}
23+
24+
private protected static void WriteClassEnd(StringBuilder builder)
25+
{
26+
builder.AppendLine(" }");
27+
builder.AppendLine("}");
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Community.VisualStudio.SourceGenerators;
4+
5+
[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "Exception is only used internally.")]
6+
public class InvalidManifestException : Exception
7+
{
8+
public InvalidManifestException(string message) : base(message) { }
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Community.VisualStudio.SourceGenerators;
2+
3+
public class Manifest
4+
{
5+
public string Id { get; set; } = "";
6+
public string Name { get; set; } = "";
7+
public string Description { get; set; } = "";
8+
public string Language { get; set; } = "";
9+
public string Author { get; set; } = "";
10+
public string Version { get; set; } = "";
11+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Text;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Text;
4+
5+
namespace Community.VisualStudio.SourceGenerators;
6+
7+
[Generator]
8+
public class ManifestGenerator : GeneratorBase, ISourceGenerator
9+
{
10+
private const string _manifestFileName = "source.extension.vsixmanifest";
11+
12+
private static readonly DiagnosticDescriptor _manifestFileNotFound = new(
13+
DiagnosticIds.CVSSG001_ManifestFileNotFound,
14+
new LocalizableResourceString(nameof(Resources.CVSSG001_Title), Resources.ResourceManager, typeof(Resources)),
15+
new LocalizableResourceString(nameof(Resources.CVSSG001_MessageFormat), Resources.ResourceManager, typeof(Resources)),
16+
CommonDiagnostics.Category,
17+
DiagnosticSeverity.Error,
18+
true
19+
);
20+
21+
private static readonly DiagnosticDescriptor _invalidManifestFile = new(
22+
DiagnosticIds.CVSSG002_InvalidManifestFile,
23+
new LocalizableResourceString(nameof(Resources.CVSSG002_Title), Resources.ResourceManager, typeof(Resources)),
24+
new LocalizableResourceString(nameof(Resources.CVSSG002_MessageFormat), Resources.ResourceManager, typeof(Resources)),
25+
CommonDiagnostics.Category,
26+
DiagnosticSeverity.Error,
27+
true
28+
);
29+
30+
public void Initialize(GeneratorInitializationContext context)
31+
{
32+
// No initialization required.
33+
}
34+
35+
public void Execute(GeneratorExecutionContext context)
36+
{
37+
AdditionalText? manifestFile = context.AdditionalFiles.FirstOrDefault(
38+
(file) => Path.GetFileName(file.Path).Equals(_manifestFileName, StringComparison.OrdinalIgnoreCase)
39+
);
40+
41+
if (manifestFile is null)
42+
{
43+
context.ReportDiagnostic(Diagnostic.Create(_manifestFileNotFound, Location.None));
44+
return;
45+
}
46+
47+
Manifest manifest;
48+
try
49+
{
50+
manifest = ManifestParser.Parse(manifestFile.GetText(context.CancellationToken)?.ToString() ?? "");
51+
}
52+
catch (InvalidManifestException ex)
53+
{
54+
context.ReportDiagnostic(Diagnostic.Create(_invalidManifestFile, Location.None, ex.Message));
55+
return;
56+
}
57+
58+
if (!manifestFile.TryGetNamespace(context, out string? generatedNamespace))
59+
{
60+
context.ReportDiagnostic(Diagnostic.Create(CommonDiagnostics.NoNamespace, Location.None, _manifestFileName));
61+
return;
62+
}
63+
64+
StringBuilder builder = new();
65+
WritePreamble(builder);
66+
WriteClassStart(builder, generatedNamespace, "Defines constants from the <c>source.extension.vsixmanifest</c> file.", "Vsix");
67+
builder.AppendLine(" /// <summary>The author of the extension.</summary>");
68+
builder.AppendLine($" public const string Author = \"{EscapeStringLiteral(manifest.Author)}\";");
69+
builder.AppendLine("");
70+
builder.AppendLine(" /// <summary>The description of the extension.</summary>");
71+
builder.AppendLine($" public const string Description = \"{EscapeStringLiteral(manifest.Description)}\";");
72+
builder.AppendLine("");
73+
builder.AppendLine(" /// <summary>The extension identifier.</summary>");
74+
builder.AppendLine($" public const string Id = \"{EscapeStringLiteral(manifest.Id)}\";");
75+
builder.AppendLine("");
76+
builder.AppendLine(" /// <summary>The default language for the extension.</summary>");
77+
builder.AppendLine($" public const string Language = \"{EscapeStringLiteral(manifest.Language)}\";");
78+
builder.AppendLine("");
79+
builder.AppendLine(" /// <summary>The name of the extension.</summary>");
80+
builder.AppendLine($" public const string Name = \"{EscapeStringLiteral(manifest.Name)}\";");
81+
builder.AppendLine("");
82+
builder.AppendLine(" /// <summary>The verison of the extension.</summary>");
83+
builder.AppendLine($" public const string Version = \"{EscapeStringLiteral(manifest.Version)}\";");
84+
WriteClassEnd(builder);
85+
86+
context.AddSource("Vsix.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8));
87+
}
88+
89+
private static object EscapeStringLiteral(string value)
90+
{
91+
return value
92+
// Backslashes need to be replaced with two backslashes.
93+
.Replace("\\", "\\\\")
94+
// Quotes need to be escaped with a backslash.
95+
.Replace("\"", "\\\"");
96+
}
97+
}

0 commit comments

Comments
 (0)