Skip to content

Commit 9c1772f

Browse files
authored
Merge pull request #1908 from PhenX/feature/analyzers
Feature/analyzers
2 parents 7a839d9 + 2a6fbc7 commit 9c1772f

15 files changed

Lines changed: 724 additions & 1 deletion

.github/dependabot.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ updates:
4848
sample-code-dependencies:
4949
patterns:
5050
- "FluentMigrator*"
51+
roslyn-analyzers-dependencies:
52+
patterns:
53+
- "Microsoft.CodeAnalysis*"
5154
other-dependencies:
5255
patterns:
5356
- "FSharp*"

FluentMigrator.sln

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gh-actions", "gh-actions",
114114
.github\workflows\pull-requests.yaml = .github\workflows\pull-requests.yaml
115115
EndProjectSection
116116
EndProject
117+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "analyzers", "analyzers", "{4C0E608C-2A95-46FD-AE82-5070B7967CDF}"
118+
EndProject
119+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMigrator.Analyzers", "src\FluentMigrator.Analyzers\FluentMigrator.Analyzers.csproj", "{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}"
120+
EndProject
121+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentMigrator.Analyzers.Tests", "test\FluentMigrator.Analyzers.Tests\FluentMigrator.Analyzers.Tests.csproj", "{E66D4318-D78D-4A0D-B876-E1932D0DACEA}"
122+
EndProject
117123
Global
118124
GlobalSection(SolutionConfigurationPlatforms) = preSolution
119125
Debug|Any CPU = Debug|Any CPU
@@ -460,6 +466,30 @@ Global
460466
{2755D82C-E45D-479B-B502-FAAF3322DD9B}.Release|x64.Build.0 = Release|Any CPU
461467
{2755D82C-E45D-479B-B502-FAAF3322DD9B}.Release|x86.ActiveCfg = Release|Any CPU
462468
{2755D82C-E45D-479B-B502-FAAF3322DD9B}.Release|x86.Build.0 = Release|Any CPU
469+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
470+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|Any CPU.Build.0 = Debug|Any CPU
471+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|x64.ActiveCfg = Debug|Any CPU
472+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|x64.Build.0 = Debug|Any CPU
473+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|x86.ActiveCfg = Debug|Any CPU
474+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Debug|x86.Build.0 = Debug|Any CPU
475+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|Any CPU.ActiveCfg = Release|Any CPU
476+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|Any CPU.Build.0 = Release|Any CPU
477+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|x64.ActiveCfg = Release|Any CPU
478+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|x64.Build.0 = Release|Any CPU
479+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|x86.ActiveCfg = Release|Any CPU
480+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439}.Release|x86.Build.0 = Release|Any CPU
481+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
482+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
483+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|x64.ActiveCfg = Debug|Any CPU
484+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|x64.Build.0 = Debug|Any CPU
485+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|x86.ActiveCfg = Debug|Any CPU
486+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Debug|x86.Build.0 = Debug|Any CPU
487+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
488+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|Any CPU.Build.0 = Release|Any CPU
489+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|x64.ActiveCfg = Release|Any CPU
490+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|x64.Build.0 = Release|Any CPU
491+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|x86.ActiveCfg = Release|Any CPU
492+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA}.Release|x86.Build.0 = Release|Any CPU
463493
EndGlobalSection
464494
GlobalSection(SolutionProperties) = preSolution
465495
HideSolutionNode = FALSE
@@ -497,6 +527,8 @@ Global
497527
{DB61C1AF-8934-4F5F-BB8A-451C817536F1} = {16DED0B3-1401-43AD-9402-3DC90918FFD7}
498528
{CD51E6A3-B43A-493B-B9FD-1C7A6E68F389} = {DE3C74CA-1396-44E6-B46B-5536BC9771DB}
499529
{2755D82C-E45D-479B-B502-FAAF3322DD9B} = {DE3C74CA-1396-44E6-B46B-5536BC9771DB}
530+
{4C3D2485-EA5E-4DFE-B7D4-1BF1F2500439} = {4C0E608C-2A95-46FD-AE82-5070B7967CDF}
531+
{E66D4318-D78D-4A0D-B876-E1932D0DACEA} = {CE354F3F-F5B9-4723-A5A1-26B37E949103}
500532
EndGlobalSection
501533
GlobalSection(ExtensibilityGlobals) = postSolution
502534
SolutionGuid = {722C9D0D-FAB6-4CCE-9A4A-56E24235A9E0}

nuget.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<configuration>
33
<packageSources>
44
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#region License
2+
// Copyright (c) 2024, Fluent Migrator Project
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#endregion
16+
17+
using Microsoft.CodeAnalysis.Diagnostics;
18+
19+
namespace FluentMigrator.Analyzers.Analysis
20+
{
21+
public abstract class FluentMigratorAnalyzer : DiagnosticAnalyzer
22+
{
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.EnableConcurrentExecution();
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
28+
context.RegisterCompilationStartAction(compilationStartContext =>
29+
{
30+
var fluentMigratorContext = new FluentMigratorContext(compilationStartContext.Compilation);
31+
AnalyzeCompilation(compilationStartContext, fluentMigratorContext);
32+
});
33+
}
34+
35+
internal abstract void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext, FluentMigratorContext fluentMigratorContext);
36+
}
37+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#region License
2+
// Copyright (c) 2024, Fluent Migrator Project
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#endregion
16+
17+
using System;
18+
using System.Collections.Generic;
19+
20+
using Microsoft.CodeAnalysis;
21+
22+
namespace FluentMigrator.Analyzers.Analysis
23+
{
24+
public class FluentMigratorContext
25+
{
26+
private readonly Lazy<INamedTypeSymbol> _lazyMigrationAttributeType;
27+
28+
internal FluentMigratorContext(Compilation compilation)
29+
{
30+
Compilation = compilation;
31+
32+
_lazyMigrationAttributeType = new Lazy<INamedTypeSymbol>(() => compilation.GetTypeByMetadataName(Constants.Types.FluentMigratorMigrationAttribute));
33+
}
34+
35+
/// <summary>
36+
/// Gets the Roslyn <see cref="Compilation"/> associated with this context.
37+
/// </summary>
38+
public Compilation Compilation { get; }
39+
40+
public INamedTypeSymbol MigrationAttributeType => _lazyMigrationAttributeType?.Value;
41+
42+
public List<MigrationClassDeclaration> Migrations { get; } = new List<MigrationClassDeclaration>();
43+
}
44+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Release 1.0
2+
3+
### New Rules
4+
5+
| Rule ID | Category | Severity | Notes |
6+
|---------|----------------|----------|------------------------------------|
7+
| FM0001 | FluentMigrator | Warning | Migration version should be unique |
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#region License
2+
// Copyright (c) 2024, Fluent Migrator Project
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#endregion
16+
17+
namespace FluentMigrator.Analyzers
18+
{
19+
internal static class Constants
20+
{
21+
internal static class Types
22+
{
23+
internal const string FluentMigratorMigrationAttribute = "FluentMigrator.MigrationAttribute";
24+
}
25+
}
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<IncludeBuildOutput>false</IncludeBuildOutput>
6+
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
7+
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
8+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
9+
<RootNamespace>FluentMigrator.Analyzers</RootNamespace>
10+
<Description>FluentMigrator is a database migration framework for .NET written in C#. The basic idea is that you can create migrations which are simply classes that derive from the Migration base class and have a Migration attribute with a unique version number attached to them. Upon executing FluentMigrator, you tell it which version to migrate to and it will run all necessary migrations in order to bring your database up to that version. In addition to forward migration support, FluentMigrator also supports different ways to execute the migrations along with selective migrations called profiles and executing arbitrary SQL.</Description>
11+
<PackageTags>fluent;migrator;fluentmigrator;migration;database;analyzer</PackageTags>
12+
<SignAssembly>true</SignAssembly>
13+
<AssemblyOriginatorKeyFile>..\..\FluentMigrator.snk</AssemblyOriginatorKeyFile>
14+
</PropertyGroup>
15+
<Import Project="$(MSBuildThisFileDirectory)../../PackageLibrary.props" />
16+
<ItemGroup>
17+
<None Include="..\..\FluentMigrator.snk" Link="FluentMigrator.snk" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
22+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" PrivateAssets="all" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<Compile Update="Resources.Designer.cs" DesignTime="True" AutoGen="True" DependentUpon="Resources.resx" />
27+
<EmbeddedResource Update="Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" />
32+
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<AdditionalFiles Include="AnalyzerReleases.*.md" />
37+
</ItemGroup>
38+
39+
</Project>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#region License
2+
// Copyright (c) 2024, Fluent Migrator Project
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#endregion
16+
17+
using System.Collections.Generic;
18+
using System.Collections.Immutable;
19+
using System.Linq;
20+
21+
using FluentMigrator.Analyzers.Analysis;
22+
23+
using Microsoft.CodeAnalysis;
24+
using Microsoft.CodeAnalysis.Diagnostics;
25+
26+
namespace FluentMigrator.Analyzers
27+
{
28+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
29+
public class MigrationAttributeVersionShouldBeUniqueAnalyzer : FluentMigratorAnalyzer
30+
{
31+
public const string DiagnosticId = "FM0001";
32+
private const string Category = "FluentMigrator";
33+
34+
// You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
35+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.MigrationAttributeVersionShouldBeUniqueAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
36+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.MigrationAttributeVersionShouldBeUniqueAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
37+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.MigrationAttributeVersionShouldBeUniqueAnalyzerDescription), Resources.ResourceManager, typeof(Resources));
38+
39+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description, customTags: "CompilationEnd");
40+
41+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
42+
43+
internal override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext, FluentMigratorContext fluentMigratorContext)
44+
{
45+
// Get all migration classes and their version number
46+
compilationStartContext.RegisterSymbolAction(symbolContext =>
47+
{
48+
var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
49+
var migrationVersion = GetMigrationAttributeVersion(fluentMigratorContext, typeSymbol.GetAttributes());
50+
if (migrationVersion == null)
51+
{
52+
return;
53+
}
54+
55+
fluentMigratorContext.Migrations.Add(new MigrationClassDeclaration(typeSymbol, migrationVersion.Value));
56+
57+
}, SymbolKind.NamedType);
58+
59+
// At the end: report duplicates
60+
compilationStartContext.RegisterCompilationEndAction(
61+
context =>
62+
{
63+
var duplicates = fluentMigratorContext.Migrations
64+
.OrderBy(m => m.Name)
65+
.GroupBy(m => m.Version)
66+
.Where(g => g.Count() > 1);
67+
68+
foreach (var duplicate in duplicates)
69+
{
70+
var groupName = string.Join(", ", duplicate.Select(d => d.Name));
71+
72+
foreach (var tuple in duplicate)
73+
{
74+
context.ReportDiagnostic(
75+
Diagnostic.Create(
76+
Rule,
77+
tuple.Location,
78+
groupName,
79+
tuple.Version
80+
)
81+
);
82+
}
83+
}
84+
});
85+
}
86+
87+
private static long? GetMigrationAttributeVersion(FluentMigratorContext fluentMigratorContext, IEnumerable<AttributeData> attributes)
88+
=> attributes
89+
.FirstOrDefault(a => fluentMigratorContext.MigrationAttributeType.IsAssignableFrom(a.AttributeClass))
90+
?.ConstructorArguments[0].Value as long?;
91+
}
92+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#region License
2+
// Copyright (c) 2024, Fluent Migrator Project
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
#endregion
16+
17+
using Microsoft.CodeAnalysis;
18+
19+
namespace FluentMigrator.Analyzers
20+
{
21+
public struct MigrationClassDeclaration
22+
{
23+
public MigrationClassDeclaration(INamedTypeSymbol typeSymbol, long migrationVersion)
24+
{
25+
Location = typeSymbol.Locations[0];
26+
Name = typeSymbol.Name;
27+
Version = migrationVersion;
28+
}
29+
30+
public string Name { get; init; }
31+
public Location Location { get; init; }
32+
public long Version { get; init; }
33+
}
34+
}

0 commit comments

Comments
 (0)