Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add analyzer for weighing-machine #109

Merged
merged 22 commits into from
Nov 30, 2021
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6c63798
Start writing code
Grenkin1988 Nov 13, 2021
3b18356
Add more analysis for properties
Grenkin1988 Nov 13, 2021
118bdd7
Provate field analysis
Grenkin1988 Nov 13, 2021
b140a1e
Add IsRoundMethodCalledInDisplayWeightProperty
Grenkin1988 Nov 13, 2021
559a7ab
Fix some comments
Grenkin1988 Nov 16, 2021
4eea02f
Merge branch 'exercism:main' into weighing-machine-analyzer
Grenkin1988 Nov 16, 2021
895d47d
Add some comments, and implemet finding correct baking field
Grenkin1988 Nov 17, 2021
41c9b12
More comments
Grenkin1988 Nov 17, 2021
6cb3e15
Include files and revert launch
Grenkin1988 Nov 17, 2021
5c1fb57
Try adding tests
Grenkin1988 Nov 18, 2021
f8772de
Remove explicit solution entries in csproj
ErikSchierboom Nov 19, 2021
a0fd8d0
Remove comments integration from analysis
ErikSchierboom Nov 19, 2021
5f43e51
Merge remote-tracking branch 'ErikSchierboom/remove-website-copy-inte…
Grenkin1988 Nov 19, 2021
8278ab6
Merge remote-tracking branch 'origin/main' into weighing-machine-anal…
Grenkin1988 Nov 19, 2021
f3716b3
Merge branch 'weighing-machine-analyzer' of https://github.com/Grenki…
Grenkin1988 Nov 19, 2021
1e537c7
All tests pass
Grenkin1988 Nov 19, 2021
0cb889d
Auto property case
Grenkin1988 Nov 19, 2021
0c72335
csharp.weighingmachine.property_setter_is_not_private
Grenkin1988 Nov 19, 2021
062d381
All tests done and fixed logic
Grenkin1988 Nov 20, 2021
1216c5c
Merge remote-tracking branch 'origin/main' into weighing-machine-anal…
Grenkin1988 Nov 25, 2021
c780424
Move some comments to shared and add approve case
Grenkin1988 Nov 27, 2021
8f9d964
Fix project and test
Grenkin1988 Nov 30, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -23,4 +23,6 @@ obj/
.ionide/

# Generated analysis files
test/*/Solutions/**/analysis.json
test/*/Solutions/**/analysis.json

launchSettings.json
39 changes: 31 additions & 8 deletions Excercism.Analyzers.CSharp.sln
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
# Visual Studio Version 17
VisualStudioVersion = 17.0.31912.275
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{07DEB1CE-A0D2-4010-AE4E-F89A77740448}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercism.Analyzers.CSharp", "src\Exercism.Analyzers.CSharp\Exercism.Analyzers.CSharp.csproj", "{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exercism.Analyzers.CSharp", "src\Exercism.Analyzers.CSharp\Exercism.Analyzers.CSharp.csproj", "{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{92305253-2BAC-427E-82F9-2B1ACDE7BB6B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercism.Analyzers.CSharp.IntegrationTests", "test\Exercism.Analyzers.CSharp.IntegrationTests\Exercism.Analyzers.CSharp.IntegrationTests.csproj", "{68A9B824-0AF2-4CE9-B2CE-15DCE08645AF}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exercism.Analyzers.CSharp.IntegrationTests", "test\Exercism.Analyzers.CSharp.IntegrationTests\Exercism.Analyzers.CSharp.IntegrationTests.csproj", "{68A9B824-0AF2-4CE9-B2CE-15DCE08645AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercism.Analyzers.CSharp.Bulk", "src\Exercism.Analyzers.CSharp.Bulk\Exercism.Analyzers.CSharp.Bulk.csproj", "{9851D7BD-FDDA-4869-B15C-608009896001}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exercism.Analyzers.CSharp.Bulk", "src\Exercism.Analyzers.CSharp.Bulk\Exercism.Analyzers.CSharp.Bulk.csproj", "{9851D7BD-FDDA-4869-B15C-608009896001}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{CB3E0F5E-0991-40F6-A3DF-2017CB694FB3}"
ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
.gitmodules = .gitmodules
.prettierignore = .prettierignore
analyze-in-bulk.ps1 = analyze-in-bulk.ps1
analyze-in-docker.ps1 = analyze-in-docker.ps1
analyze.ps1 = analyze.ps1
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
Dockerfile = Dockerfile
format.ps1 = format.ps1
LICENSE = LICENSE
README.md = README.md
run.sh = run.sh
test.ps1 = test.ps1
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -35,9 +52,15 @@ Global
{9851D7BD-FDDA-4869-B15C-608009896001}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9851D7BD-FDDA-4869-B15C-608009896001}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E} = {07DEB1CE-A0D2-4010-AE4E-F89A77740448}
{68A9B824-0AF2-4CE9-B2CE-15DCE08645AF} = {92305253-2BAC-427E-82F9-2B1ACDE7BB6B}
{9851D7BD-FDDA-4869-B15C-608009896001} = {07DEB1CE-A0D2-4010-AE4E-F89A77740448}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5DCA2FF8-9813-4F76-9135-F313BF8AE432}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -34,5 +34,17 @@ public static SolutionComment MissingMethod(string method) =>

public static SolutionComment InvalidMethodSignature(string method, string signature) =>
new SolutionComment("csharp.general.invalid_method_signature", new SolutionCommentParameter(Name, method), new SolutionCommentParameter(Signature, signature));

public static SolutionComment MissingProperty(string property) =>
new("csharp.general.missing_property", new SolutionCommentParameter(Name, property));

public static SolutionComment PropertyIsNotAutoProperty(string name) =>
new("csharp.general.property_is_not_auto_property", new SolutionCommentParameter(Name, name));

public static SolutionComment PrecisionPropertyHasNonPrivateSetter(string name) =>
new("csharp.general.property_setter_is_not_private", new SolutionCommentParameter(Name, name));

public static SolutionComment PropertyBetterUseInitializer(string name) =>
new("csharp.general.property_better_use_initializer", new SolutionCommentParameter(Name, name));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Exercism.Analyzers.CSharp.Analyzers.Shared;

using static Exercism.Analyzers.CSharp.Analyzers.Shared.SharedComments;
using static Exercism.Analyzers.CSharp.Analyzers.WeighingMachine.WeighingMachineSolution;

namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
{
internal class WeighingMachineAnalyzer : SharedAnalyzer<WeighingMachineSolution>
{
protected override SolutionAnalysis DisapproveWhenInvalid(WeighingMachineSolution solution)
{
if (solution.MissingWeighingMachineClass)
{
solution.AddComment(MissingClass(WeighingMachineClassName));
return solution.Disapprove();
}

foreach (var missing in solution.MissingRequiredProperties())
{
solution.AddComment(MissingProperty(missing));
return solution.Disapprove();
}

if (!solution.PrecisionIsAutoProperty)
{
solution.AddComment(SharedComments.PropertyIsNotAutoProperty("Precision"));
}

if (!solution.TareAdjustmentIsAutoProperty)
{
solution.AddComment(SharedComments.PropertyIsNotAutoProperty("TareAdjustment"));
}

if (solution.PrecisionPropertyHasNonPrivateSetter())
{
solution.AddComment(SharedComments.PrecisionPropertyHasNonPrivateSetter("Precision"));
}

if (!solution.WeightFieldNameIsPrivate(out var fieldName) && !string.IsNullOrWhiteSpace(fieldName))
{
solution.AddComment(UsePrivateVisibility(fieldName));
}

if (!solution.IsRoundMethodCalledInDisplayWeightProperty())
{
solution.AddComment(WeighingMachineComments.RoundMethodNotCalledInDisplayWeightProperty);
}

return solution.HasComments
? solution.Disapprove()
: solution.ContinueAnalysis();
}

protected override SolutionAnalysis ApproveWhenValid(WeighingMachineSolution solution)
{
if (!solution.TareAdjustmentHasInitializer)
{
solution.AddComment(SharedComments.PropertyBetterUseInitializer("TareAdjustment"));
}

return solution.Approve();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

using static Exercism.Analyzers.CSharp.Analyzers.Shared.SharedCommentParameters;

namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
{
internal class WeighingMachineComments
{
public static SolutionComment RoundMethodNotCalledInDisplayWeightProperty =
new("csharp.weighingmachine.round_called_in_display_weight");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;

using Exercism.Analyzers.CSharp.Syntax;
using Exercism.Analyzers.CSharp.Syntax.Comparison;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
{
internal class WeighingMachineSolution : Solution
{
public const string WeighingMachineClassName = "WeighingMachine";
public const string PrecisionPropertyName = "Precision";
public const string WeightPropertyName = "Weight";
public const string TareAdjustmentPropertyName = "TareAdjustment";
public const string DisplayWeightPropertyName = "DisplayWeight";

public WeighingMachineSolution(Solution solution) : base(solution)
{
}

private ClassDeclarationSyntax WeighingMachineClass => SyntaxRoot.GetClass(WeighingMachineClassName);

public bool MissingWeighingMachineClass => WeighingMachineClass is null;

private PropertyDeclarationSyntax PrecisionProperty => WeighingMachineClass?.GetProperty(PrecisionPropertyName);

private PropertyDeclarationSyntax WeightProperty => WeighingMachineClass?.GetProperty(WeightPropertyName);

private PropertyDeclarationSyntax TareAdjustmentProperty => WeighingMachineClass?.GetProperty(TareAdjustmentPropertyName);

private PropertyDeclarationSyntax DisplayWeightProperty => WeighingMachineClass?.GetProperty(DisplayWeightPropertyName);

public IEnumerable<string> MissingRequiredProperties()
{
if (PrecisionProperty is null) yield return PrecisionPropertyName;
if (WeightProperty is null) yield return WeightPropertyName;
if (TareAdjustmentProperty is null) yield return TareAdjustmentPropertyName;
if (DisplayWeightProperty is null) yield return DisplayWeightPropertyName;
}

public bool PrecisionIsAutoProperty => PrecisionProperty?.IsAutoProperty() == true;

public bool TareAdjustmentIsAutoProperty => TareAdjustmentProperty?.IsAutoProperty() == true;

public bool TareAdjustmentHasInitializer => TareAdjustmentProperty?.HasInitializer() == true;

public bool PrecisionPropertyHasNonPrivateSetter()
{
var setAccessor = PrecisionProperty.GetSetAccessor();
return setAccessor is not null &&
(setAccessor.Modifiers.Count == 0 ||
!setAccessor.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PrivateKeyword)));
}

public bool WeightFieldNameIsPrivate(out string fieldName)
{
fieldName = WeightProperty.GetBakingFieldName();

if (string.IsNullOrEmpty(fieldName))
{
return false;
}

return WeighingMachineClass?.GetField(fieldName)?.IsPrivate() ?? false;
}

public bool IsRoundMethodCalledInDisplayWeightProperty() =>
DisplayWeightProperty?.GetMethodCalled("Round") is not null;
}
}
1 change: 1 addition & 0 deletions src/Exercism.Analyzers.CSharp/Exercises.cs
Original file line number Diff line number Diff line change
@@ -5,5 +5,6 @@ internal static class Exercises
public const string TwoFer = "two-fer";
public const string Gigasecond = "gigasecond";
public const string Leap = "leap";
public const string WeighingMachine = "weighing-machine";
}
}
3 changes: 3 additions & 0 deletions src/Exercism.Analyzers.CSharp/SolutionAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
using Exercism.Analyzers.CSharp.Analyzers.Gigasecond;
using Exercism.Analyzers.CSharp.Analyzers.Leap;
using Exercism.Analyzers.CSharp.Analyzers.TwoFer;
using Exercism.Analyzers.CSharp.Analyzers.WeighingMachine;

namespace Exercism.Analyzers.CSharp
{
@@ -17,6 +18,8 @@ public static SolutionAnalysis Analyze(Solution solution)
return new GigasecondAnalyzer().Analyze(new GigasecondSolution(solution));
case Exercises.Leap:
return new LeapAnalyzer().Analyze(new LeapSolution(solution));
case Exercises.WeighingMachine:
return new WeighingMachineAnalyzer().Analyze(new WeighingMachineSolution(solution));
default:
return new DefaultExerciseAnalyzer().Analyze(new DefaultSolution(solution));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Linq;

using Exercism.Analyzers.CSharp.Syntax.Rewriting;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Exercism.Analyzers.CSharp.Syntax.Comparison
{
@@ -20,5 +23,12 @@ public static bool IsEquivalentToNormalized(SyntaxNode node, SyntaxNode other)
}

private static SyntaxNode Normalize(this SyntaxNode node) => NormalizeSyntaxRewriter.Visit(node);

public static MemberAccessExpressionSyntax GetMethodCalled(this SyntaxNode syntaxNode, string methodName) =>
syntaxNode
.DescendantNodes<InvocationExpressionSyntax>()
.Select(s => s.Expression)
.OfType<MemberAccessExpressionSyntax>()
.FirstOrDefault(s => s.Name.Identifier.Text == methodName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Exercism.Analyzers.CSharp.Syntax
{
public static class PropertyDeclarationSyntaxExtensions
{
public const string GetAccessorName = "get";
public const string SetAccessorName = "set";

public static bool IsAutoProperty(this PropertyDeclarationSyntax property)
{
if (property.AccessorList != null)
{
return property.AccessorList.Accessors.All(a => a.Body is null && a.ExpressionBody is null);
}

return property.ExpressionBody is null;
}

public static bool HasInitializer(this PropertyDeclarationSyntax property) => property?.Initializer is not null;

public static AccessorDeclarationSyntax GetAccessor(this PropertyDeclarationSyntax property, SyntaxKind accessor)
{
return property?.AccessorList?.Accessors.FirstOrDefault(ac => ac.IsKind(accessor));
}

public static AccessorDeclarationSyntax GetGetAccessor(this PropertyDeclarationSyntax property) =>
property?.GetAccessor(SyntaxKind.GetAccessorDeclaration);

public static AccessorDeclarationSyntax GetSetAccessor(this PropertyDeclarationSyntax property) =>
property?.GetAccessor(SyntaxKind.SetAccessorDeclaration);

public static string GetBakingFieldName(this PropertyDeclarationSyntax property)
{
var get = property.GetGetAccessor();
var returns = get?.Body.DescendantNodes<ReturnStatementSyntax>().FirstOrDefault();
var fieldIdentifier = returns?.Expression as IdentifierNameSyntax;
if (fieldIdentifier is not null)
{
return fieldIdentifier.Identifier.ValueText;
}

var set = property.GetSetAccessor();
var setValue = set?.Body
.DescendantNodes<AssignmentExpressionSyntax>()
.FirstOrDefault(s => s.OperatorToken.IsKind(SyntaxKind.EqualsToken)
&& s.Right is IdentifierNameSyntax ident && ident.Identifier.ValueText == "value");

if (setValue is not null && setValue.Left is IdentifierNameSyntax ident)
{
return ident.Identifier.ValueText;
}

return null;
}
}
}
28 changes: 28 additions & 0 deletions src/Exercism.Analyzers.CSharp/Syntax/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
@@ -34,6 +34,34 @@ public static IEnumerable<MethodDeclarationSyntax> GetMethods(this SyntaxNode sy
.DescendantNodes<MethodDeclarationSyntax>()
.Where(syntax => syntax.Identifier.Text == methodName) ?? Enumerable.Empty<MethodDeclarationSyntax>();

public static PropertyDeclarationSyntax GetProperty(this SyntaxNode syntaxNode, string propertyName) =>
syntaxNode?
.DescendantNodes<PropertyDeclarationSyntax>()
.FirstOrDefault(syntax => syntax.Identifier.Text == propertyName);

public static IEnumerable<PropertyDeclarationSyntax> GetProperties(this SyntaxNode syntaxNode, string propertyName) =>
syntaxNode?
.DescendantNodes<PropertyDeclarationSyntax>()
.Where(syntax => syntax.Identifier.Text == propertyName) ?? Enumerable.Empty<PropertyDeclarationSyntax>();

public static FieldDeclarationSyntax GetField(this SyntaxNode syntaxNode, string variable) =>
syntaxNode?
.DescendantNodes<VariableDeclaratorSyntax>()
.FirstOrDefault(syntax => IsEqualFieldName(syntax.Identifier.Text, variable))
?.FieldDeclaration();

public static IEnumerable<FieldDeclarationSyntax> GetFields(this SyntaxNode syntaxNode, string variable) =>
syntaxNode?
.DescendantNodes<VariableDeclaratorSyntax>()
.Where(syntax => IsEqualFieldName(syntax.Identifier.Text, variable))
.Select(v => v?.FieldDeclaration()) ?? Enumerable.Empty<FieldDeclarationSyntax>();

public static bool IsEqualFieldName(string name, string expectedName)
{
var expected = new[] { expectedName, expectedName.StartsWith("_") ? expectedName.Substring(1) : "_" + expectedName };
return expected.Contains(name);
}

public static bool AssignsToIdentifier(this SyntaxNode syntaxNode, IdentifierNameSyntax identifierName) =>
syntaxNode?
.DescendantNodes<AssignmentExpressionSyntax>()
Loading