Skip to content

Commit 543efff

Browse files
Add analyzer for weighing-machine (#109)
Add analyzer for weighing-machine Co-authored-by: Erik Schierboom <[email protected]>
1 parent b71b69c commit 543efff

File tree

36 files changed

+821
-9
lines changed

36 files changed

+821
-9
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ obj/
2323
.ionide/
2424

2525
# Generated analysis files
26-
test/*/Solutions/**/analysis.json
26+
test/*/Solutions/**/analysis.json
27+
28+
launchSettings.json

Excercism.Analyzers.CSharp.sln

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26124.0
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31912.275
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{07DEB1CE-A0D2-4010-AE4E-F89A77740448}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exercism.Analyzers.CSharp", "src\Exercism.Analyzers.CSharp\Exercism.Analyzers.CSharp.csproj", "{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exercism.Analyzers.CSharp", "src\Exercism.Analyzers.CSharp\Exercism.Analyzers.CSharp.csproj", "{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}"
99
EndProject
1010
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{92305253-2BAC-427E-82F9-2B1ACDE7BB6B}"
1111
EndProject
12-
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}"
12+
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}"
1313
EndProject
14-
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}"
14+
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}"
15+
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{CB3E0F5E-0991-40F6-A3DF-2017CB694FB3}"
17+
ProjectSection(SolutionItems) = preProject
18+
.dockerignore = .dockerignore
19+
.editorconfig = .editorconfig
20+
.gitattributes = .gitattributes
21+
.gitignore = .gitignore
22+
.gitmodules = .gitmodules
23+
.prettierignore = .prettierignore
24+
analyze-in-bulk.ps1 = analyze-in-bulk.ps1
25+
analyze-in-docker.ps1 = analyze-in-docker.ps1
26+
analyze.ps1 = analyze.ps1
27+
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
28+
Dockerfile = Dockerfile
29+
format.ps1 = format.ps1
30+
LICENSE = LICENSE
31+
README.md = README.md
32+
run.sh = run.sh
33+
test.ps1 = test.ps1
34+
EndProjectSection
1535
EndProject
1636
Global
1737
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1838
Debug|Any CPU = Debug|Any CPU
1939
Release|Any CPU = Release|Any CPU
2040
EndGlobalSection
21-
GlobalSection(SolutionProperties) = preSolution
22-
HideSolutionNode = FALSE
23-
EndGlobalSection
2441
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2542
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2643
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -35,9 +52,15 @@ Global
3552
{9851D7BD-FDDA-4869-B15C-608009896001}.Release|Any CPU.ActiveCfg = Release|Any CPU
3653
{9851D7BD-FDDA-4869-B15C-608009896001}.Release|Any CPU.Build.0 = Release|Any CPU
3754
EndGlobalSection
55+
GlobalSection(SolutionProperties) = preSolution
56+
HideSolutionNode = FALSE
57+
EndGlobalSection
3858
GlobalSection(NestedProjects) = preSolution
3959
{0BD06845-367F-4F0A-9BC8-55B1A5D55E2E} = {07DEB1CE-A0D2-4010-AE4E-F89A77740448}
4060
{68A9B824-0AF2-4CE9-B2CE-15DCE08645AF} = {92305253-2BAC-427E-82F9-2B1ACDE7BB6B}
4161
{9851D7BD-FDDA-4869-B15C-608009896001} = {07DEB1CE-A0D2-4010-AE4E-F89A77740448}
4262
EndGlobalSection
63+
GlobalSection(ExtensibilityGlobals) = postSolution
64+
SolutionGuid = {5DCA2FF8-9813-4F76-9135-F313BF8AE432}
65+
EndGlobalSection
4366
EndGlobal

src/Exercism.Analyzers.CSharp/Analyzers/Shared/SharedComments.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,17 @@ public static SolutionComment MissingMethod(string method) =>
3434

3535
public static SolutionComment InvalidMethodSignature(string method, string signature) =>
3636
new SolutionComment("csharp.general.invalid_method_signature", new SolutionCommentParameter(Name, method), new SolutionCommentParameter(Signature, signature));
37+
38+
public static SolutionComment MissingProperty(string property) =>
39+
new("csharp.general.missing_property", new SolutionCommentParameter(Name, property));
40+
41+
public static SolutionComment PropertyIsNotAutoProperty(string name) =>
42+
new("csharp.general.property_is_not_auto_property", new SolutionCommentParameter(Name, name));
43+
44+
public static SolutionComment PrecisionPropertyHasNonPrivateSetter(string name) =>
45+
new("csharp.general.property_setter_is_not_private", new SolutionCommentParameter(Name, name));
46+
47+
public static SolutionComment PropertyBetterUseInitializer(string name) =>
48+
new("csharp.general.property_better_use_initializer", new SolutionCommentParameter(Name, name));
3749
}
3850
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Exercism.Analyzers.CSharp.Analyzers.Shared;
2+
3+
using static Exercism.Analyzers.CSharp.Analyzers.Shared.SharedComments;
4+
using static Exercism.Analyzers.CSharp.Analyzers.WeighingMachine.WeighingMachineSolution;
5+
6+
namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
7+
{
8+
internal class WeighingMachineAnalyzer : SharedAnalyzer<WeighingMachineSolution>
9+
{
10+
protected override SolutionAnalysis DisapproveWhenInvalid(WeighingMachineSolution solution)
11+
{
12+
if (solution.MissingWeighingMachineClass)
13+
{
14+
solution.AddComment(MissingClass(WeighingMachineClassName));
15+
return solution.Disapprove();
16+
}
17+
18+
foreach (var missing in solution.MissingRequiredProperties())
19+
{
20+
solution.AddComment(MissingProperty(missing));
21+
return solution.Disapprove();
22+
}
23+
24+
if (!solution.PrecisionIsAutoProperty)
25+
{
26+
solution.AddComment(SharedComments.PropertyIsNotAutoProperty("Precision"));
27+
}
28+
29+
if (!solution.TareAdjustmentIsAutoProperty)
30+
{
31+
solution.AddComment(SharedComments.PropertyIsNotAutoProperty("TareAdjustment"));
32+
}
33+
34+
if (solution.PrecisionPropertyHasNonPrivateSetter())
35+
{
36+
solution.AddComment(SharedComments.PrecisionPropertyHasNonPrivateSetter("Precision"));
37+
}
38+
39+
if (!solution.WeightFieldNameIsPrivate(out var fieldName) && !string.IsNullOrWhiteSpace(fieldName))
40+
{
41+
solution.AddComment(UsePrivateVisibility(fieldName));
42+
}
43+
44+
if (!solution.IsRoundMethodCalledInDisplayWeightProperty())
45+
{
46+
solution.AddComment(WeighingMachineComments.RoundMethodNotCalledInDisplayWeightProperty);
47+
}
48+
49+
return solution.HasComments
50+
? solution.Disapprove()
51+
: solution.ContinueAnalysis();
52+
}
53+
54+
protected override SolutionAnalysis ApproveWhenValid(WeighingMachineSolution solution)
55+
{
56+
if (!solution.TareAdjustmentHasInitializer)
57+
{
58+
solution.AddComment(SharedComments.PropertyBetterUseInitializer("TareAdjustment"));
59+
}
60+
61+
return solution.Approve();
62+
}
63+
}
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+

2+
using static Exercism.Analyzers.CSharp.Analyzers.Shared.SharedCommentParameters;
3+
4+
namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
5+
{
6+
internal class WeighingMachineComments
7+
{
8+
public static SolutionComment RoundMethodNotCalledInDisplayWeightProperty =
9+
new("csharp.weighingmachine.round_called_in_display_weight");
10+
}
11+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
using Exercism.Analyzers.CSharp.Syntax;
5+
using Exercism.Analyzers.CSharp.Syntax.Comparison;
6+
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
10+
namespace Exercism.Analyzers.CSharp.Analyzers.WeighingMachine
11+
{
12+
internal class WeighingMachineSolution : Solution
13+
{
14+
public const string WeighingMachineClassName = "WeighingMachine";
15+
public const string PrecisionPropertyName = "Precision";
16+
public const string WeightPropertyName = "Weight";
17+
public const string TareAdjustmentPropertyName = "TareAdjustment";
18+
public const string DisplayWeightPropertyName = "DisplayWeight";
19+
20+
public WeighingMachineSolution(Solution solution) : base(solution)
21+
{
22+
}
23+
24+
private ClassDeclarationSyntax WeighingMachineClass => SyntaxRoot.GetClass(WeighingMachineClassName);
25+
26+
public bool MissingWeighingMachineClass => WeighingMachineClass is null;
27+
28+
private PropertyDeclarationSyntax PrecisionProperty => WeighingMachineClass?.GetProperty(PrecisionPropertyName);
29+
30+
private PropertyDeclarationSyntax WeightProperty => WeighingMachineClass?.GetProperty(WeightPropertyName);
31+
32+
private PropertyDeclarationSyntax TareAdjustmentProperty => WeighingMachineClass?.GetProperty(TareAdjustmentPropertyName);
33+
34+
private PropertyDeclarationSyntax DisplayWeightProperty => WeighingMachineClass?.GetProperty(DisplayWeightPropertyName);
35+
36+
public IEnumerable<string> MissingRequiredProperties()
37+
{
38+
if (PrecisionProperty is null) yield return PrecisionPropertyName;
39+
if (WeightProperty is null) yield return WeightPropertyName;
40+
if (TareAdjustmentProperty is null) yield return TareAdjustmentPropertyName;
41+
if (DisplayWeightProperty is null) yield return DisplayWeightPropertyName;
42+
}
43+
44+
public bool PrecisionIsAutoProperty => PrecisionProperty?.IsAutoProperty() == true;
45+
46+
public bool TareAdjustmentIsAutoProperty => TareAdjustmentProperty?.IsAutoProperty() == true;
47+
48+
public bool TareAdjustmentHasInitializer => TareAdjustmentProperty?.HasInitializer() == true;
49+
50+
public bool PrecisionPropertyHasNonPrivateSetter()
51+
{
52+
var setAccessor = PrecisionProperty.GetSetAccessor();
53+
return setAccessor is not null &&
54+
(setAccessor.Modifiers.Count == 0 ||
55+
!setAccessor.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PrivateKeyword)));
56+
}
57+
58+
public bool WeightFieldNameIsPrivate(out string fieldName)
59+
{
60+
fieldName = WeightProperty.GetBakingFieldName();
61+
62+
if (string.IsNullOrEmpty(fieldName))
63+
{
64+
return false;
65+
}
66+
67+
return WeighingMachineClass?.GetField(fieldName)?.IsPrivate() ?? false;
68+
}
69+
70+
public bool IsRoundMethodCalledInDisplayWeightProperty() =>
71+
DisplayWeightProperty?.GetMethodCalled("Round") is not null;
72+
}
73+
}

src/Exercism.Analyzers.CSharp/Exercises.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ internal static class Exercises
55
public const string TwoFer = "two-fer";
66
public const string Gigasecond = "gigasecond";
77
public const string Leap = "leap";
8+
public const string WeighingMachine = "weighing-machine";
89
}
910
}

src/Exercism.Analyzers.CSharp/SolutionAnalyzer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Exercism.Analyzers.CSharp.Analyzers.Gigasecond;
33
using Exercism.Analyzers.CSharp.Analyzers.Leap;
44
using Exercism.Analyzers.CSharp.Analyzers.TwoFer;
5+
using Exercism.Analyzers.CSharp.Analyzers.WeighingMachine;
56

67
namespace Exercism.Analyzers.CSharp
78
{
@@ -17,6 +18,8 @@ public static SolutionAnalysis Analyze(Solution solution)
1718
return new GigasecondAnalyzer().Analyze(new GigasecondSolution(solution));
1819
case Exercises.Leap:
1920
return new LeapAnalyzer().Analyze(new LeapSolution(solution));
21+
case Exercises.WeighingMachine:
22+
return new WeighingMachineAnalyzer().Analyze(new WeighingMachineSolution(solution));
2023
default:
2124
return new DefaultExerciseAnalyzer().Analyze(new DefaultSolution(solution));
2225
}

src/Exercism.Analyzers.CSharp/Syntax/Comparison/SyntaxNodeComparer.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
using System.Linq;
2+
13
using Exercism.Analyzers.CSharp.Syntax.Rewriting;
24

35
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
47

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

2225
private static SyntaxNode Normalize(this SyntaxNode node) => NormalizeSyntaxRewriter.Visit(node);
26+
27+
public static MemberAccessExpressionSyntax GetMethodCalled(this SyntaxNode syntaxNode, string methodName) =>
28+
syntaxNode
29+
.DescendantNodes<InvocationExpressionSyntax>()
30+
.Select(s => s.Expression)
31+
.OfType<MemberAccessExpressionSyntax>()
32+
.FirstOrDefault(s => s.Name.Identifier.Text == methodName);
2333
}
2434
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Linq;
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
7+
namespace Exercism.Analyzers.CSharp.Syntax
8+
{
9+
public static class PropertyDeclarationSyntaxExtensions
10+
{
11+
public const string GetAccessorName = "get";
12+
public const string SetAccessorName = "set";
13+
14+
public static bool IsAutoProperty(this PropertyDeclarationSyntax property)
15+
{
16+
if (property.AccessorList != null)
17+
{
18+
return property.AccessorList.Accessors.All(a => a.Body is null && a.ExpressionBody is null);
19+
}
20+
21+
return property.ExpressionBody is null;
22+
}
23+
24+
public static bool HasInitializer(this PropertyDeclarationSyntax property) => property?.Initializer is not null;
25+
26+
public static AccessorDeclarationSyntax GetAccessor(this PropertyDeclarationSyntax property, SyntaxKind accessor)
27+
{
28+
return property?.AccessorList?.Accessors.FirstOrDefault(ac => ac.IsKind(accessor));
29+
}
30+
31+
public static AccessorDeclarationSyntax GetGetAccessor(this PropertyDeclarationSyntax property) =>
32+
property?.GetAccessor(SyntaxKind.GetAccessorDeclaration);
33+
34+
public static AccessorDeclarationSyntax GetSetAccessor(this PropertyDeclarationSyntax property) =>
35+
property?.GetAccessor(SyntaxKind.SetAccessorDeclaration);
36+
37+
public static string GetBakingFieldName(this PropertyDeclarationSyntax property)
38+
{
39+
var get = property.GetGetAccessor();
40+
var returns = get?.Body.DescendantNodes<ReturnStatementSyntax>().FirstOrDefault();
41+
var fieldIdentifier = returns?.Expression as IdentifierNameSyntax;
42+
if (fieldIdentifier is not null)
43+
{
44+
return fieldIdentifier.Identifier.ValueText;
45+
}
46+
47+
var set = property.GetSetAccessor();
48+
var setValue = set?.Body
49+
.DescendantNodes<AssignmentExpressionSyntax>()
50+
.FirstOrDefault(s => s.OperatorToken.IsKind(SyntaxKind.EqualsToken)
51+
&& s.Right is IdentifierNameSyntax ident && ident.Identifier.ValueText == "value");
52+
53+
if (setValue is not null && setValue.Left is IdentifierNameSyntax ident)
54+
{
55+
return ident.Identifier.ValueText;
56+
}
57+
58+
return null;
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)