mirror of
https://github.com/Pathoschild/SMAPI.git
synced 2026-10-12 02:08:51 +00:00
add mod code analyzers to detect implicit net field conversion issues (#471)
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!--
|
||||
|
||||
This build task is run from the ModBuildConfig project after it's been compiled, and copies the
|
||||
package files to the bin\Pathoschild.Stardew.ModBuildConfig folder.
|
||||
|
||||
-->
|
||||
<Target Name="AfterBuild">
|
||||
<PropertyGroup>
|
||||
<PackagePath>$(SolutionDir)\..\bin\Pathoschild.Stardew.ModBuildConfig</PackagePath>
|
||||
</PropertyGroup>
|
||||
<RemoveDir Directories="$(PackagePath)" />
|
||||
<Copy SourceFiles="$(ProjectDir)/package.nuspec" DestinationFolder="$(PackagePath)" />
|
||||
<Copy SourceFiles="$(ProjectDir)/build/smapi.targets" DestinationFiles="$(PackagePath)/build/Pathoschild.Stardew.ModBuildConfig.targets" />
|
||||
<Copy SourceFiles="$(TargetDir)/StardewModdingAPI.ModBuildConfig.dll" DestinationFiles="$(PackagePath)/build/StardewModdingAPI.ModBuildConfig.dll" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/bin/netstandard1.3/StardewModdingAPI.ModBuildConfig.Analyzer.dll" DestinationFiles="$(PackagePath)/analyzers/dotnet/cs/StardewModdingAPI.ModBuildConfig.Analyzer.dll" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1" DestinationFiles="$(PackagePath)/tools/install.ps1" />
|
||||
<Copy SourceFiles="$(SolutionDir)/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1" DestinationFiles="$(PackagePath)/tools/uninstall.ps1" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -3,15 +3,16 @@ for SMAPI mods.
|
||||
|
||||
The package...
|
||||
|
||||
* lets your code compile on any computer (Linux/Mac/Windows) without needing to change the assembly
|
||||
references or game path.
|
||||
* packages the mod into the game's `Mods` folder when you rebuild the code (configurable).
|
||||
* configures Visual Studio so you can debug into the mod code when the game is running (_Windows
|
||||
only_).
|
||||
* detects your game install path;
|
||||
* adds the assembly references you need (with automatic support for Linux/Mac/Windows);
|
||||
* packages the mod into your `Mods` folder when you rebuild the code (configurable);
|
||||
* configures Visual Studio to enable debugging into the code when the game is running (_Windows only_);
|
||||
* adds C# analyzers to warn for Stardew Valley-specific issues.
|
||||
|
||||
## Contents
|
||||
* [Install](#install)
|
||||
* [Configure](#configure)
|
||||
* [Code analysis warnings](#code-analysis-warnings)
|
||||
* [Troubleshoot](#troubleshoot)
|
||||
* [Release notes](#release-notes)
|
||||
|
||||
@@ -121,7 +122,7 @@ The configuration will check your custom path first, then fall back to the defau
|
||||
still compile on a different computer).
|
||||
|
||||
### Unit test projects
|
||||
**(upcoming in 2.0.3)**
|
||||
**(upcoming in 2.1)**
|
||||
|
||||
You can use the package in unit test projects too. Its optional unit test mode...
|
||||
|
||||
@@ -134,15 +135,63 @@ To enable it, add this above the first `</PropertyGroup>` in your `.csproj`:
|
||||
<ModUnitTests>True</ModUnitTests>
|
||||
```
|
||||
|
||||
## Code warnings
|
||||
### Overview
|
||||
The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example:
|
||||

|
||||
|
||||
You can hide the warnings...
|
||||
* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning);
|
||||
* for a method using this attribute:
|
||||
```cs
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "SMAPI001")] // implicit net field conversion
|
||||
```
|
||||
* for an entire project:
|
||||
1. Expand the _References_ node for the project in Visual Studio.
|
||||
2. Right-click on _Analyzers_ and choose _Open Active Rule Set_.
|
||||
4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide.
|
||||
|
||||
See below for help with each specific warning.
|
||||
|
||||
### SMAPI001
|
||||
**Implicit net field conversion:**
|
||||
> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but
|
||||
> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual
|
||||
> value instead to avoid bugs.
|
||||
|
||||
Stardew Valley uses a set of net fields (like `NetBool` or `NetInt`) to handle multiplayer sync.
|
||||
These types can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`),
|
||||
but their conversion rules can be unintuitive and error-prone. For example,
|
||||
`item?.category == null && item?.category != null` can both be true at once, and
|
||||
`building.indoors != null` will be true for a null value in some cases.
|
||||
|
||||
Suggested fix:
|
||||
* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property:
|
||||
```c#
|
||||
if (building.indoors.Value == null)
|
||||
```
|
||||
Or convert the value before comparison:
|
||||
```c#
|
||||
GameLocation indoors = building.indoors;
|
||||
if(indoors == null)
|
||||
// ...
|
||||
```
|
||||
* For a value type (i.e. one that can't contain `null`), make sure to check for null first if
|
||||
applicable:
|
||||
```cs
|
||||
if (item != null && item.category == 0)
|
||||
```
|
||||
|
||||
## Troubleshoot
|
||||
### "Failed to find the game install path"
|
||||
That error means the package couldn't find your game. You can specify the game path yourself; see
|
||||
_[Game path](#game-path)_ above.
|
||||
|
||||
## Release notes
|
||||
### 2.0.3 alpha
|
||||
### 2.1 alpha
|
||||
* Added support for Stardew Valley 1.3.
|
||||
* Added support for unit test projects.
|
||||
* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
|
||||
|
||||
### 2.0.2
|
||||
* Fixed compatibility issue on Linux.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
@@ -20,6 +20,7 @@ mods, this section isn't relevant to you; see the main README to use or create m
|
||||
* [Development](#development-2)
|
||||
* [Local development](#local-development)
|
||||
* [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk)
|
||||
* [Mod build config package](#mod-build-config-package)
|
||||
|
||||
# SMAPI
|
||||
## Development
|
||||
@@ -222,3 +223,31 @@ property name | description
|
||||
`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`.
|
||||
`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info.
|
||||
`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info.
|
||||
|
||||
## Mod build config package
|
||||
### Overview
|
||||
The mod build config package is a NuGet package that mods reference to automatically set up
|
||||
references, configure the build, and add analyzers specific to Stardew Valley mods.
|
||||
|
||||
This involves three projects:
|
||||
|
||||
project | purpose
|
||||
------------------------------------------------- | ----------------
|
||||
`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc).
|
||||
`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio.
|
||||
`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers.
|
||||
|
||||
When the projects are built, the relevant files are copied into `bin/Pathoschild.Stardew.ModBuildConfig`.
|
||||
|
||||
### Preparing a build
|
||||
To prepare a build of the NuGet package:
|
||||
1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli).
|
||||
1. Change the version and release notes in `package.nuspec`.
|
||||
2. Rebuild the solution in _Release_ mode.
|
||||
3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command:
|
||||
```bash
|
||||
nuget.exe pack
|
||||
```
|
||||
|
||||
That will create a `Pathoschild.Stardew.ModBuildConfig-<version>.nupkg` file in the same directory
|
||||
which can be uploaded to NuGet or referenced directly.
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// <generated />
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
||||
{
|
||||
/// <summary>
|
||||
/// Location where the diagnostic appears, as determined by path, line number, and column number.
|
||||
/// </summary>
|
||||
public struct DiagnosticResultLocation
|
||||
{
|
||||
public DiagnosticResultLocation(string path, int line, int column)
|
||||
{
|
||||
if (line < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
|
||||
}
|
||||
|
||||
if (column < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
|
||||
}
|
||||
|
||||
this.Path = path;
|
||||
this.Line = line;
|
||||
this.Column = column;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct that stores information about a Diagnostic appearing in a source
|
||||
/// </summary>
|
||||
public struct DiagnosticResult
|
||||
{
|
||||
private DiagnosticResultLocation[] locations;
|
||||
|
||||
public DiagnosticResultLocation[] Locations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.locations == null)
|
||||
{
|
||||
this.locations = new DiagnosticResultLocation[] { };
|
||||
}
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.locations = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DiagnosticSeverity Severity { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Path : "";
|
||||
}
|
||||
}
|
||||
|
||||
public int Line
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int Column
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// <generated />
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for turning strings into documents and getting the diagnostics on them
|
||||
/// All methods are static
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
|
||||
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
|
||||
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
|
||||
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
|
||||
|
||||
internal static string DefaultFilePathPrefix = "Test";
|
||||
internal static string CSharpDefaultFileExt = "cs";
|
||||
internal static string VisualBasicDefaultExt = "vb";
|
||||
internal static string TestProjectName = "TestProject";
|
||||
|
||||
#region Get Diagnostics
|
||||
|
||||
/// <summary>
|
||||
/// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source classes are in</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the sources</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
|
||||
/// The returned diagnostics are then ordered by location in the source document.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer to run on the documents</param>
|
||||
/// <param name="documents">The Documents that the analyzer will be run on</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
|
||||
{
|
||||
var projects = new HashSet<Project>();
|
||||
foreach (var document in documents)
|
||||
{
|
||||
projects.Add(document.Project);
|
||||
}
|
||||
|
||||
var diagnostics = new List<Diagnostic>();
|
||||
foreach (var project in projects)
|
||||
{
|
||||
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
|
||||
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
|
||||
foreach (var diag in diags)
|
||||
{
|
||||
if (diag.Location == Location.None || diag.Location.IsInMetadata)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < documents.Length; i++)
|
||||
{
|
||||
var document = documents[i];
|
||||
var tree = document.GetSyntaxTreeAsync().Result;
|
||||
if (tree == diag.Location.SourceTree)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = SortDiagnostics(diagnostics);
|
||||
diagnostics.Clear();
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort diagnostics by location in source document
|
||||
/// </summary>
|
||||
/// <param name="diagnostics">The list of Diagnostics to be sorted</param>
|
||||
/// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
|
||||
private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
|
||||
{
|
||||
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set up compilation and documents
|
||||
/// <summary>
|
||||
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
|
||||
private static Document[] GetDocuments(string[] sources, string language)
|
||||
{
|
||||
if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
|
||||
{
|
||||
throw new ArgumentException("Unsupported Language");
|
||||
}
|
||||
|
||||
var project = CreateProject(sources, language);
|
||||
var documents = project.Documents.ToArray();
|
||||
|
||||
if (sources.Length != documents.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Document from a string through creating a project that contains it.
|
||||
/// </summary>
|
||||
/// <param name="source">Classes in the form of a string</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Document created from the source string</returns>
|
||||
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
|
||||
{
|
||||
return CreateProject(new[] { source }, language).Documents.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a project using the inputted strings as sources.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Project created out of the Documents created from the source strings</returns>
|
||||
private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
|
||||
{
|
||||
string fileNamePrefix = DefaultFilePathPrefix;
|
||||
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
|
||||
|
||||
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
|
||||
|
||||
var solution = new AdhocWorkspace()
|
||||
.CurrentSolution
|
||||
.AddProject(projectId, TestProjectName, TestProjectName, language)
|
||||
.AddMetadataReference(projectId, CorlibReference)
|
||||
.AddMetadataReference(projectId, SystemCoreReference)
|
||||
.AddMetadataReference(projectId, CSharpSymbolsReference)
|
||||
.AddMetadataReference(projectId, CodeAnalysisReference);
|
||||
|
||||
int count = 0;
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var newFileName = fileNamePrefix + count + "." + fileExt;
|
||||
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
|
||||
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
|
||||
count++;
|
||||
}
|
||||
return solution.GetProject(projectId);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// <generated />
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
||||
{
|
||||
/// <summary>
|
||||
/// Superclass of all Unit Tests for DiagnosticAnalyzers
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
#region To be implemented by Test classes
|
||||
/// <summary>
|
||||
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Verifier wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="source">A class in the form of a string to run the analyzer on</param>
|
||||
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
|
||||
/// then verifies each of them.
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="language">The language of the classes represented by the source strings</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the source code</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
|
||||
{
|
||||
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
|
||||
VerifyDiagnosticResults(diagnostics, analyzer, expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actual comparisons and verifications
|
||||
/// <summary>
|
||||
/// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
|
||||
/// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
|
||||
/// </summary>
|
||||
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
|
||||
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
|
||||
{
|
||||
int expectedCount = expectedResults.Count();
|
||||
int actualCount = actualResults.Count();
|
||||
|
||||
if (expectedCount != actualCount)
|
||||
{
|
||||
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
|
||||
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
|
||||
}
|
||||
|
||||
for (int i = 0; i < expectedResults.Length; i++)
|
||||
{
|
||||
var actual = actualResults.ElementAt(i);
|
||||
var expected = expectedResults[i];
|
||||
|
||||
if (expected.Line == -1 && expected.Column == -1)
|
||||
{
|
||||
if (actual.Location != Location.None)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
|
||||
var additionalLocations = actual.AdditionalLocations.ToArray();
|
||||
|
||||
if (additionalLocations.Length != expected.Locations.Length - 1)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
|
||||
expected.Locations.Length - 1, additionalLocations.Length,
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
for (int j = 0; j < additionalLocations.Length; ++j)
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (actual.Id != expected.Id)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.Severity != expected.Severity)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.GetMessage() != expected.Message)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="diagnostic">The diagnostic that was found in the code</param>
|
||||
/// <param name="actual">The Location of the Diagnostic found in the code</param>
|
||||
/// <param name="expected">The DiagnosticResultLocation that should have been found</param>
|
||||
private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
|
||||
{
|
||||
var actualSpan = actual.GetLineSpan();
|
||||
|
||||
Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
|
||||
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
|
||||
|
||||
var actualLinePosition = actualSpan.StartLinePosition;
|
||||
|
||||
// Only check line position if there is an actual line in the real diagnostic
|
||||
if (actualLinePosition.Line > 0)
|
||||
{
|
||||
if (actualLinePosition.Line + 1 != expected.Line)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
|
||||
// Only check column position if there is an actual column position in the real diagnostic
|
||||
if (actualLinePosition.Character > 0)
|
||||
{
|
||||
if (actualLinePosition.Character + 1 != expected.Column)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Formatting Diagnostics
|
||||
/// <summary>
|
||||
/// Helper method to format a Diagnostic into an easily readable string
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that this verifier tests</param>
|
||||
/// <param name="diagnostics">The Diagnostics to be formatted</param>
|
||||
/// <returns>The Diagnostics formatted as a string</returns>
|
||||
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < diagnostics.Length; ++i)
|
||||
{
|
||||
builder.AppendLine("// " + diagnostics[i].ToString());
|
||||
|
||||
var analyzerType = analyzer.GetType();
|
||||
var rules = analyzer.SupportedDiagnostics;
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule != null && rule.Id == diagnostics[i].Id)
|
||||
{
|
||||
var location = diagnostics[i].Location;
|
||||
if (location == Location.None)
|
||||
{
|
||||
builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsTrue(location.IsInSource,
|
||||
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
|
||||
|
||||
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
|
||||
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
|
||||
|
||||
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
|
||||
resultMethodName,
|
||||
linePosition.Line + 1,
|
||||
linePosition.Character + 1,
|
||||
analyzerType.Name,
|
||||
rule.Id);
|
||||
}
|
||||
|
||||
if (i != diagnostics.Length - 1)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
|
||||
<PackageReference Include="NUnit" Version="3.10.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI.ModBuildConfig.Analyzer\StardewModdingAPI.ModBuildConfig.Analyzer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,118 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using NUnit.Framework;
|
||||
using SMAPI.ModBuildConfig.Analyzer.Tests.Framework;
|
||||
using StardewModdingAPI.ModBuildConfig.Analyzer;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
||||
{
|
||||
/// <summary>Unit tests for the C# analyzers.</summary>
|
||||
[TestFixture]
|
||||
public class UnitTests : DiagnosticVerifier
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>Sample C# code which contains a simplified representation of Stardew Valley's <c>Netcode</c> types, and sample mod code with a {{test-code}} placeholder for the code being tested.</summary>
|
||||
const string SampleProgram = @"
|
||||
using System;
|
||||
using Netcode;
|
||||
|
||||
namespace Netcode
|
||||
{
|
||||
public class NetInt : NetFieldBase<int, NetInt> { }
|
||||
public class NetRef : NetFieldBase<object, NetRef> { }
|
||||
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
|
||||
{
|
||||
public T Value { get; set; }
|
||||
public static implicit operator T(NetFieldBase<T, TSelf> field) => field.Value;
|
||||
}
|
||||
}
|
||||
|
||||
namespace SampleMod
|
||||
{
|
||||
class Item
|
||||
{
|
||||
public NetInt category { get; } = new NetInt { Value = 42 };
|
||||
}
|
||||
|
||||
class ModEntry
|
||||
{
|
||||
public void Entry()
|
||||
{
|
||||
NetInt intField = new NetInt { Value = 42 };
|
||||
NetRef refField = new NetRef { Value = null };
|
||||
Item item = null;
|
||||
|
||||
// this line should raise diagnostics
|
||||
{{test-code}} // line 32
|
||||
|
||||
// these lines should not
|
||||
if ((int)field != 42);
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
/// <summary>The line number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
|
||||
private const int SampleCodeLine = 32;
|
||||
|
||||
/// <summary>The column number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
|
||||
private const int SampleCodeColumn = 25;
|
||||
|
||||
|
||||
/*********
|
||||
** Unit tests
|
||||
*********/
|
||||
/// <summary>Test that no diagnostics are raised for an empty code block.</summary>
|
||||
[TestCase]
|
||||
public void EmptyCode_HasNoDiagnostics()
|
||||
{
|
||||
// arrange
|
||||
string test = @"";
|
||||
|
||||
// assert
|
||||
this.VerifyCSharpDiagnostic(test);
|
||||
}
|
||||
|
||||
/// <summary>Test that the expected diagnostic message is raised.</summary>
|
||||
/// <param name="codeText">The code line to test.</param>
|
||||
/// <param name="column">The column within the code line where the diagnostic message should be reported.</param>
|
||||
/// <param name="expression">The expression which should be reported.</param>
|
||||
/// <param name="fromType">The source type name which should be reported.</param>
|
||||
/// <param name="toType">The target type name which should be reported.</param>
|
||||
[TestCase("if (intField < 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (intField <= 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (intField > 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (intField >= 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (intField == 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (intField != 42);", 4, "intField", "NetInt", "Int32")]
|
||||
[TestCase("if (refField != null);", 4, "refField", "NetRef", "Object")]
|
||||
[TestCase("if (item?.category != 42);", 4, "item?.category", "NetInt", "Int32")]
|
||||
public void ImplicitComparisons_RaiseDiagnostics(string codeText, int column, string expression, string fromType, string toType)
|
||||
{
|
||||
// arrange
|
||||
string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
{
|
||||
Id = "SMAPI001",
|
||||
Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/SMAPI001 for details.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", UnitTests.SampleCodeLine, UnitTests.SampleCodeColumn + column) }
|
||||
};
|
||||
|
||||
// assert
|
||||
this.VerifyCSharpDiagnostic(code, expected);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Helpers
|
||||
*********/
|
||||
/// <summary>Get the analyzer being tested.</summary>
|
||||
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return new ImplicitNetFieldCastAnalyzer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
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 ImplicitNetFieldCastAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
/*********
|
||||
** Properties
|
||||
*********/
|
||||
/// <summary>The namespace for Stardew Valley's <c>Netcode</c> types.</summary>
|
||||
private const string NetcodeNamespace = "Netcode";
|
||||
|
||||
/// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
|
||||
private static readonly DiagnosticDescriptor Rule = 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,
|
||||
description: "",
|
||||
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 ImplicitNetFieldCastAnalyzer()
|
||||
{
|
||||
this.SupportedDiagnostics = ImmutableArray.Create(ImplicitNetFieldCastAnalyzer.Rule);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(
|
||||
this.Analyse,
|
||||
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 applicable.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
private void Analyse(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
BinaryExpressionSyntax node = (BinaryExpressionSyntax)context.Node;
|
||||
bool leftHasWarning = this.Analyze(context, node.Left);
|
||||
if (!leftHasWarning)
|
||||
this.Analyze(context, node.Right);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Analyse one operand in a binary expression (like <c>a</c> and <c>b</c> in <c>a == b</c>) and add a diagnostic message if applicable.</summary>
|
||||
/// <param name="context">The analysis context.</param>
|
||||
/// <param name="operand">The operand expression.</param>
|
||||
/// <returns>Returns whether a diagnostic message was raised.</returns>
|
||||
private bool Analyze(SyntaxNodeAnalysisContext context, ExpressionSyntax operand)
|
||||
{
|
||||
const string netcodeNamespace = ImplicitNetFieldCastAnalyzer.NetcodeNamespace;
|
||||
|
||||
TypeInfo operandType = context.SemanticModel.GetTypeInfo(operand);
|
||||
string fromNamespace = operandType.Type?.ContainingNamespace?.Name;
|
||||
string toNamespace = operandType.ConvertedType?.ContainingNamespace?.Name;
|
||||
if (fromNamespace == netcodeNamespace && fromNamespace != toNamespace && toNamespace != null)
|
||||
{
|
||||
string fromTypeName = operandType.Type.Name;
|
||||
string toTypeName = operandType.ConvertedType.Name;
|
||||
context.ReportDiagnostic(Diagnostic.Create(ImplicitNetFieldCastAnalyzer.Rule, context.Node.GetLocation(), operand, fromTypeName, toTypeName));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyTitle("SMAPI.ModBuildConfig.Analyzer")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,5 @@
|
||||
</ItemGroup>
|
||||
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\build\prepare-nuget-package.targets" />
|
||||
</Project>
|
||||
@@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Pathoschild.Stardew.ModBuildConfig</id>
|
||||
<version>2.0.3-alpha20180325</version>
|
||||
<version>2.1-alpha20180409</version>
|
||||
<title>Build package for SMAPI mods</title>
|
||||
<authors>Pathoschild</authors>
|
||||
<owners>Pathoschild</owners>
|
||||
@@ -12,28 +12,10 @@
|
||||
<iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl>
|
||||
<description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods.</description>
|
||||
<releaseNotes>
|
||||
2.0:
|
||||
- Added: mods are now copied into the `Mods` folder automatically (configurable).
|
||||
- Added: release zips are now created automatically in your build output folder (configurable).
|
||||
- Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
|
||||
- Added mod's version to release zip filename.
|
||||
- Improved errors to simplify troubleshooting.
|
||||
- Fixed release zip not having a mod folder.
|
||||
- Fixed release zip failing if mod name contains characters that aren't valid in a filename.
|
||||
|
||||
2.0.1:
|
||||
- Fixed mod deploy failing to create subfolders if they don't already exist.
|
||||
|
||||
2.0.2:
|
||||
- Fixed compatibility issue on Linux.
|
||||
|
||||
2.0.3:
|
||||
2.1:
|
||||
- Added support for Stardew Valley 1.3.
|
||||
- Added support for unit test projects.
|
||||
- Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
|
||||
</releaseNotes>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="build/smapi.targets" target="build/Pathoschild.Stardew.ModBuildConfig.targets" />
|
||||
<file src="bin/StardewModdingAPI.ModBuildConfig.dll" target="build/StardewModdingAPI.ModBuildConfig.dll" />
|
||||
</files>
|
||||
</package>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<Name>StardewModdingAPI</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
</Project>
|
||||
@@ -45,9 +45,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5
|
||||
..\build\common.targets = ..\build\common.targets
|
||||
..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs
|
||||
..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets
|
||||
..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{EA4F1E80-743F-4A1D-9757-AE66904A196A}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1} = {80AD8528-AA49-4731-B4A6-C691845815A1}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildConfig.Analyzer", "SMAPI.ModBuildConfig.Analyzer\StardewModdingAPI.ModBuildConfig.Analyzer.csproj", "{80AD8528-AA49-4731-B4A6-C691845815A1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SharedMSBuildProjectFiles) = preSolution
|
||||
@@ -137,6 +145,30 @@ Global
|
||||
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.Build.0 = Release|x86
|
||||
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86
|
||||
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{80AD8528-AA49-4731-B4A6-C691845815A1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -147,6 +179,7 @@ Global
|
||||
{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
||||
{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA}
|
||||
{09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA}
|
||||
{0CF97929-B0D0-4D73-B7BF-4FF7191035F9} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC}
|
||||
|
||||
Reference in New Issue
Block a user