summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig.Analyzer
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.ModBuildConfig.Analyzer')
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs271
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs98
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/Properties/AssemblyInfo.cs4
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj23
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps158
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps165
6 files changed, 519 insertions, 0 deletions
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
new file mode 100644
index 00000000..915a50e8
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs
@@ -0,0 +1,271 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace StardewModdingAPI.ModBuildConfig.Analyzer
+{
+ /// <summary>Detects implicit conversion from Stardew Valley's <c>Netcode</c> types. These have very unintuitive implicit conversion rules, so mod authors should always explicitly convert the type with appropriate null checks.</summary>
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class NetFieldAnalyzer : DiagnosticAnalyzer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The namespace for Stardew Valley's <c>Netcode</c> types.</summary>
+ private const string NetcodeNamespace = "Netcode";
+
+ /// <summary>Maps net fields to their equivalent non-net properties where available.</summary>
+ private readonly IDictionary<string, string> NetFieldWrapperProperties = new Dictionary<string, string>
+ {
+ // Character
+ ["StardewValley.Character::currentLocationRef"] = "currentLocation",
+ ["StardewValley.Character::facingDirection"] = "FacingDirection",
+ ["StardewValley.Character::name"] = "Name",
+ ["StardewValley.Character::position"] = "Position",
+ ["StardewValley.Character::scale"] = "Scale",
+ ["StardewValley.Character::speed"] = "Speed",
+ ["StardewValley.Character::sprite"] = "Sprite",
+
+ // Chest
+ ["StardewValley.Objects.Chest::tint"] = "Tint",
+
+ // Farmer
+ ["StardewValley.Farmer::houseUpgradeLevel"] = "HouseUpgradeLevel",
+ ["StardewValley.Farmer::isMale"] = "IsMale",
+ ["StardewValley.Farmer::items"] = "Items",
+ ["StardewValley.Farmer::magneticRadius"] = "MagneticRadius",
+ ["StardewValley.Farmer::stamina"] = "Stamina",
+ ["StardewValley.Farmer::uniqueMultiplayerID"] = "UniqueMultiplayerID",
+ ["StardewValley.Farmer::usingTool"] = "UsingTool",
+
+ // Forest
+ ["StardewValley.Locations.Forest::netTravelingMerchantDay"] = "travelingMerchantDay",
+ ["StardewValley.Locations.Forest::netLog"] = "log",
+
+ // FruitTree
+ ["StardewValley.TerrainFeatures.FruitTree::greenHouseTileTree"] = "GreenHouseTileTree",
+ ["StardewValley.TerrainFeatures.FruitTree::greenHouseTree"] = "GreenHouseTree",
+
+ // GameLocation
+ ["StardewValley.GameLocation::isFarm"] = "IsFarm",
+ ["StardewValley.GameLocation::isOutdoors"] = "IsOutdoors",
+ ["StardewValley.GameLocation::lightLevel"] = "LightLevel",
+ ["StardewValley.GameLocation::name"] = "Name",
+
+ // Item
+ ["StardewValley.Item::category"] = "Category",
+ ["StardewValley.Item::netName"] = "Name",
+ ["StardewValley.Item::parentSheetIndex"] = "ParentSheetIndex",
+ ["StardewValley.Item::specialVariable"] = "SpecialVariable",
+
+ // Junimo
+ ["StardewValley.Characters.Junimo::eventActor"] = "EventActor",
+
+ // LightSource
+ ["StardewValley.LightSource::identifier"] = "Identifier",
+
+ // Monster
+ ["StardewValley.Monsters.Monster::damageToFarmer"] = "DamageToFarmer",
+ ["StardewValley.Monsters.Monster::experienceGained"] = "ExperienceGained",
+ ["StardewValley.Monsters.Monster::health"] = "Health",
+ ["StardewValley.Monsters.Monster::maxHealth"] = "MaxHealth",
+ ["StardewValley.Monsters.Monster::netFocusedOnFarmers"] = "focusedOnFarmers",
+ ["StardewValley.Monsters.Monster::netWildernessFarmMonster"] = "wildernessFarmMonster",
+ ["StardewValley.Monsters.Monster::slipperiness"] = "Slipperiness",
+
+ // NPC
+ ["StardewValley.NPC::age"] = "Age",
+ ["StardewValley.NPC::birthday_Day"] = "Birthday_Day",
+ ["StardewValley.NPC::birthday_Season"] = "Birthday_Season",
+ ["StardewValley.NPC::breather"] = "Breather",
+ ["StardewValley.NPC::defaultMap"] = "DefaultMap",
+ ["StardewValley.NPC::gender"] = "Gender",
+ ["StardewValley.NPC::hideShadow"] = "HideShadow",
+ ["StardewValley.NPC::isInvisible"] = "IsInvisible",
+ ["StardewValley.NPC::isWalkingTowardPlayer"] = "IsWalkingTowardPlayer",
+ ["StardewValley.NPC::manners"] = "Manners",
+ ["StardewValley.NPC::optimism"] = "Optimism",
+ ["StardewValley.NPC::socialAnxiety"] = "SocialAnxiety",
+
+ // Object
+ ["StardewValley.Object::canBeGrabbed"] = "CanBeGrabbed",
+ ["StardewValley.Object::canBeSetDown"] = "CanBeSetDown",
+ ["StardewValley.Object::edibility"] = "Edibility",
+ ["StardewValley.Object::flipped"] = "Flipped",
+ ["StardewValley.Object::fragility"] = "Fragility",
+ ["StardewValley.Object::hasBeenPickedUpByFarmer"] = "HasBeenPickedUpByFarmer",
+ ["StardewValley.Object::isHoedirt"] = "IsHoeDirt",
+ ["StardewValley.Object::isOn"] = "IsOn",
+ ["StardewValley.Object::isRecipe"] = "IsRecipe",
+ ["StardewValley.Object::isSpawnedObject"] = "IsSpawnedObject",
+ ["StardewValley.Object::minutesUntilReady"] = "MinutesUntilReady",
+ ["StardewValley.Object::netName"] = "name",
+ ["StardewValley.Object::price"] = "Price",
+ ["StardewValley.Object::quality"] = "Quality",
+ ["StardewValley.Object::scale"] = "Scale",
+ ["StardewValley.Object::stack"] = "Stack",
+ ["StardewValley.Object::tileLocation"] = "TileLocation",
+ ["StardewValley.Object::type"] = "Type",
+
+ // Projectile
+ ["StardewValley.Projectiles.Projectile::ignoreLocationCollision"] = "IgnoreLocationCollision",
+
+ // Tool
+ ["StardewValley.Tool::currentParentTileIndex"] = "CurrentParentTileIndex",
+ ["StardewValley.Tool::indexOfMenuItemView"] = "IndexOfMenuItemView",
+ ["StardewValley.Tool::initialParentTileIndex"] = "InitialParentTileIndex",
+ ["StardewValley.Tool::instantUse"] = "InstantUse",
+ ["StardewValley.Tool::netName"] = "BaseName",
+ ["StardewValley.Tool::stackable"] = "Stackable",
+ ["StardewValley.Tool::upgradeLevel"] = "UpgradeLevel"
+ };
+
+ /// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
+ private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
+ {
+ ["SMAPI001"] = new DiagnosticDescriptor(
+ id: "SMAPI001",
+ title: "Netcode types shouldn't be implicitly converted",
+ messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/smapi001 for details.",
+ category: "SMAPI.CommonErrors",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ helpLinkUri: "https://smapi.io/buildmsg/smapi001"
+ ),
+ ["SMAPI002"] = new DiagnosticDescriptor(
+ id: "SMAPI002",
+ title: "Avoid Netcode types when possible",
+ messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/smapi002 for details.",
+ category: "SMAPI.CommonErrors",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ helpLinkUri: "https://smapi.io/buildmsg/smapi001"
+ )
+ };
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The descriptors for the diagnostics that this analyzer is capable of producing.</summary>
+ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public NetFieldAnalyzer()
+ {
+ this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values);
+ }
+
+ /// <summary>Called once at session start to register actions in the analysis context.</summary>
+ /// <param name="context">The analysis context.</param>
+ public override void Initialize(AnalysisContext context)
+ {
+ // SMAPI002: avoid net fields if possible
+ context.RegisterSyntaxNodeAction(
+ this.AnalyzeAvoidableNetField,
+ SyntaxKind.SimpleMemberAccessExpression
+ );
+
+ // SMAPI001: avoid implicit net field conversion
+ context.RegisterSyntaxNodeAction(
+ this.AnalyseNetFieldConversions,
+ SyntaxKind.EqualsExpression,
+ SyntaxKind.NotEqualsExpression,
+ SyntaxKind.GreaterThanExpression,
+ SyntaxKind.GreaterThanOrEqualExpression,
+ SyntaxKind.LessThanExpression,
+ SyntaxKind.LessThanOrEqualExpression
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Analyse a syntax node and add a diagnostic message if it references a net field when there's a non-net equivalent available.</summary>
+ /// <param name="context">The analysis context.</param>
+ private void AnalyzeAvoidableNetField(SyntaxNodeAnalysisContext context)
+ {
+ try
+ {
+ // check member type
+ MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node;
+ TypeInfo memberType = context.SemanticModel.GetTypeInfo(node);
+ if (!this.IsNetType(memberType.Type))
+ return;
+
+ // get reference info
+ ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type;
+ string propertyName = node.Name.Identifier.Text;
+
+ // suggest replacement
+ for (ITypeSymbol type = declaringType; type != null; type = type.BaseType)
+ {
+ if (this.NetFieldWrapperProperties.TryGetValue($"{type}::{propertyName}", out string suggestedPropertyName))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI002"], context.Node.GetLocation(), node, memberType.Type.Name, suggestedPropertyName));
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
+ }
+ }
+
+ /// <summary>Analyse a syntax node and add a diagnostic message if it implicitly converts a net field.</summary>
+ /// <param name="context">The analysis context.</param>
+ private void AnalyseNetFieldConversions(SyntaxNodeAnalysisContext context)
+ {
+ try
+ {
+ BinaryExpressionSyntax binaryExpression = (BinaryExpressionSyntax)context.Node;
+ foreach (var pair in new[] { Tuple.Create(binaryExpression.Left, binaryExpression.Right), Tuple.Create(binaryExpression.Right, binaryExpression.Left) })
+ {
+ // get node info
+ ExpressionSyntax curExpression = pair.Item1; // the side of the comparison being examined
+ ExpressionSyntax otherExpression = pair.Item2; // the other side
+ TypeInfo typeInfo = context.SemanticModel.GetTypeInfo(curExpression);
+ if (!this.IsNetType(typeInfo.Type))
+ continue;
+
+ // warn for implicit conversion
+ if (!this.IsNetType(typeInfo.ConvertedType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, typeInfo.ConvertedType));
+ break;
+ }
+
+ // warn for comparison to null
+ // An expression like `building.indoors != null` will sometimes convert `building.indoors` to NetFieldBase instead of object before comparison. Haven't reproduced this in unit tests yet.
+ Optional<object> otherValue = context.SemanticModel.GetConstantValue(otherExpression);
+ if (otherValue.HasValue && otherValue.Value == null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI001"], context.Node.GetLocation(), curExpression, typeInfo.Type.Name, "null"));
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
+ }
+ }
+
+ /// <summary>Get whether a type symbol references a <c>Netcode</c> type.</summary>
+ /// <param name="typeSymbol">The type symbol.</param>
+ private bool IsNetType(ITypeSymbol typeSymbol)
+ {
+ return typeSymbol?.ContainingNamespace?.Name == NetFieldAnalyzer.NetcodeNamespace;
+ }
+ }
+}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
new file mode 100644
index 00000000..00565329
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace StardewModdingAPI.ModBuildConfig.Analyzer
+{
+ /// <summary>Detects references to a field which has been replaced.</summary>
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ObsoleteFieldAnalyzer : DiagnosticAnalyzer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Maps obsolete fields/properties to their non-obsolete equivalent.</summary>
+ private readonly IDictionary<string, string> ReplacedFields = new Dictionary<string, string>
+ {
+ // Farmer
+ ["StardewValley.Farmer::friendships"] = "friendshipData"
+ };
+
+ /// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
+ private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
+ {
+ ["SMAPI003"] = new DiagnosticDescriptor(
+ id: "SMAPI003",
+ title: "Reference to obsolete field",
+ messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/smapi003 for details.",
+ category: "SMAPI.CommonErrors",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ helpLinkUri: "https://smapi.io/buildmsg/smapi003"
+ )
+ };
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The descriptors for the diagnostics that this analyzer is capable of producing.</summary>
+ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public ObsoleteFieldAnalyzer()
+ {
+ this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values);
+ }
+
+ /// <summary>Called once at session start to register actions in the analysis context.</summary>
+ /// <param name="context">The analysis context.</param>
+ public override void Initialize(AnalysisContext context)
+ {
+ // SMAPI003: avoid obsolete fields
+ context.RegisterSyntaxNodeAction(
+ this.AnalyzeObsoleteFields,
+ SyntaxKind.SimpleMemberAccessExpression
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Analyse a syntax node and add a diagnostic message if it references an obsolete field.</summary>
+ /// <param name="context">The analysis context.</param>
+ private void AnalyzeObsoleteFields(SyntaxNodeAnalysisContext context)
+ {
+ try
+ {
+ // get reference info
+ MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node;
+ ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type;
+ string propertyName = node.Name.Identifier.Text;
+
+ // suggest replacement
+ for (ITypeSymbol type = declaringType; type != null; type = type.BaseType)
+ {
+ if (this.ReplacedFields.TryGetValue($"{type}::{propertyName}", out string replacement))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI003"], context.Node.GetLocation(), $"{type}.{propertyName}", replacement));
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
+ }
+ }
+ }
+}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig.Analyzer/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..1cc41000
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Reflection;
+
+[assembly: AssemblyTitle("SMAPI.ModBuildConfig.Analyzer")]
+[assembly: AssemblyDescription("")]
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj
new file mode 100644
index 00000000..c32343e3
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj
@@ -0,0 +1,23 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard1.3</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <IncludeBuildOutput>false</IncludeBuildOutput>
+ <OutputPath>bin</OutputPath>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" PrivateAssets="all" />
+ <PackageReference Update="NETStandard.Library" PrivateAssets="all" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 b/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1
new file mode 100644
index 00000000..ff051759
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1
@@ -0,0 +1,58 @@
+param($installPath, $toolsPath, $package, $project)
+
+if($project.Object.SupportsPackageDependencyResolution)
+{
+ if($project.Object.SupportsPackageDependencyResolution())
+ {
+ # Do not install analyzers via install.ps1, instead let the project system handle it.
+ return
+ }
+}
+
+$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ if (Test-Path $analyzersPath)
+ {
+ # Install the language agnostic analyzers.
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
+
+# $project.Type gives the language name like (C# or VB.NET)
+$languageFolder = ""
+if($project.Type -eq "C#")
+{
+ $languageFolder = "cs"
+}
+if($project.Type -eq "VB.NET")
+{
+ $languageFolder = "vb"
+}
+if($languageFolder -eq "")
+{
+ return
+}
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Install language specific analyzers.
+ $languageAnalyzersPath = join-path $analyzersPath $languageFolder
+ if (Test-Path $languageAnalyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 b/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1
new file mode 100644
index 00000000..4bed3337
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1
@@ -0,0 +1,65 @@
+param($installPath, $toolsPath, $package, $project)
+
+if($project.Object.SupportsPackageDependencyResolution)
+{
+ if($project.Object.SupportsPackageDependencyResolution())
+ {
+ # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it.
+ return
+ }
+}
+
+$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Uninstall the language agnostic analyzers.
+ if (Test-Path $analyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
+ }
+ }
+ }
+}
+
+# $project.Type gives the language name like (C# or VB.NET)
+$languageFolder = ""
+if($project.Type -eq "C#")
+{
+ $languageFolder = "cs"
+}
+if($project.Type -eq "VB.NET")
+{
+ $languageFolder = "vb"
+}
+if($languageFolder -eq "")
+{
+ return
+}
+
+foreach($analyzersPath in $analyzersPaths)
+{
+ # Uninstall language specific analyzers.
+ $languageAnalyzersPath = join-path $analyzersPath $languageFolder
+ if (Test-Path $languageAnalyzersPath)
+ {
+ foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
+ {
+ if($project.Object.AnalyzerReferences)
+ {
+ try
+ {
+ $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
+ }
+ catch
+ {
+
+ }
+ }
+ }
+ }
+}