Skip to content

Commit 941c3b5

Browse files
authored
Merge pull request #622 from Acumatica/feature/ATR-777-dev-graphs-and-graph-extensions-not-sealed
Feature/atr 777 dev graphs and graph extensions not sealed
2 parents b854a25 + 692584b commit 941c3b5

File tree

16 files changed

+366
-24
lines changed

16 files changed

+366
-24
lines changed

docs/Summary.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ Acuminator does not perform static analysis of projects whose names contain `Tes
113113
| [PX1110](diagnostics/PX1110.md) | The DAC has a DAC field with the `PXDBLocalizableString` attribute. Therefore, this DAC must declare a `NoteID` DAC field. | Error | Available |
114114
| [PX1111](diagnostics/PX1111.md) | The primary DAC of a processing view must contain the `NoteID` field. | Error | Unavailable |
115115
| [PX1112](diagnostics/PX1112.md) | Graphs and graph extensions with generic type parameters must be abstract. | Error | Available |
116+
| [PX1113](diagnostics/PX1113.md) | Graphs and graph extensions should not be `sealed` types. | Warning | Available |

docs/diagnostics/PX1113.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PX1113
2+
This document describes the PX1113 diagnostic.
3+
4+
## Summary
5+
6+
| Code | Short Description | Type | Code Fix |
7+
| ------ | --------------------------------------------------------- | ------- | --------- |
8+
| PX1113 | Graphs and graph extensions should not be `sealed` types. | Warning | Available |
9+
10+
## Diagnostic Description
11+
12+
The PX1113 diagnostic detects graphs and graph extensions marked with the `sealed` modifier.
13+
14+
Graphs and graph extensions in Acumatica Framework must not be `sealed` types because the framework's customization mechanism depends on the ability to inherit from and extend existing business logic components.
15+
When the `PXGraph.CreateInstance` factory method creates a graph instance, the Acumatica runtime dynamically generates derived types from the instantiated graph and its associated graph extensions.
16+
Within these dynamically generated types, the runtime applies and combines logic from applicable graph extensions.
17+
18+
Declaring graphs or graph extensions as `sealed` prevents this dynamic type generation, making customization impossible and causing runtime errors when attempting to extend such types.
19+
This goes against Acumatica Framework design best practices which promote extensibility and reusability of business logic. Thus, the diagnostic PX1113 issues a warning for any graph or graph extension declared as `sealed`.
20+
21+
In a rare scenario when the `sealed` modifier is intentionally used to prevent the extension of the business logic, developers should suppress the alert from PX1113 diagnostic with Acuminator suppression comment.
22+
23+
## Code Fix Behavior
24+
25+
The PX1113 code fix automatically removes the `sealed` modifier from the graph or graph extension declaration.
26+
27+
## Example of Incorrect Code
28+
29+
```C#
30+
using PX.Data;
31+
32+
namespace Examples
33+
{
34+
// Sealed graph - reports PX1113
35+
public sealed class OrderGraph<TOrderDac> : PXGraph<GenericOrderGraph<TOrderDac>>
36+
where TOrderDac : PXBqlTable, IBqlTable, new()
37+
{
38+
// Business logic
39+
}
40+
}
41+
```
42+
43+
## Example of Correct Code after Code Fix
44+
45+
```C#
46+
using PX.Data;
47+
48+
namespace Examples
49+
{
50+
public class OrderGraph<TOrderDac> : PXGraph<GenericOrderGraph<TOrderDac>>
51+
where TOrderDac : PXBqlTable, IBqlTable, new()
52+
{
53+
// Business logic
54+
}
55+
}
56+
```
57+
58+
## Related Articles
59+
60+
- [Business Logic Implementation](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=b02bd27f-3e86-4210-9e0f-c0df7aa701d9)
61+
- [Use of Generic Graph Extensions by the System](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=eece8358-f6ea-47e8-899a-d466eccbc14e)
62+
- [Reusing Business Logic](https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=76b8c160-89bd-4501-9f9f-1dabc648d417)

src/Acuminator/Acuminator.Analyzers/DiagnosticsShortName.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Acuminator/Acuminator.Analyzers/DiagnosticsShortName.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,7 @@
465465
<data name="PX1112" xml:space="preserve">
466466
<value>GenericNonAbstractGraphOrGraphExtension</value>
467467
</data>
468+
<data name="PX1113" xml:space="preserve">
469+
<value>SealedGraphOrGraphExtension</value>
470+
</data>
468471
</root>

src/Acuminator/Acuminator.Analyzers/Resources.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Acuminator/Acuminator.Analyzers/Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,12 @@ Reason: {2}</value>
875875
<data name="PX1112Title" xml:space="preserve">
876876
<value>Graphs and graph extensions with generic type parameters must be abstract</value>
877877
</data>
878+
<data name="PX1113Fix" xml:space="preserve">
879+
<value>Remove "sealed" modifier from the type</value>
880+
</data>
881+
<data name="PX1113Title" xml:space="preserve">
882+
<value>Graphs and graph extensions should not be sealed types</value>
883+
</data>
878884
<data name="SuppressDiagnosticGroupCodeActionTitle" xml:space="preserve">
879885
<value>Suppress the {0} diagnostic with Acuminator</value>
880886
</data>

src/Acuminator/Acuminator.Analyzers/StaticAnalysis/DeclarationAnalysisGraphAndDac/GraphAndGraphExtensionDeclarationAnalyzer.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
5-
using System.Threading;
66

77
using Acuminator.Analyzers.StaticAnalysis.PXGraph;
88
using Acuminator.Utilities.Common;
@@ -22,31 +22,52 @@ public class GraphAndGraphExtensionDeclarationAnalyzer : PXGraphAggregatedAnalyz
2222
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
2323
ImmutableArray.Create
2424
(
25-
Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract
25+
Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract,
26+
Descriptors.PX1113_SealedGraphsAndGraphExtensions
2627
);
2728

2829
public override void Analyze(SymbolAnalysisContext context, PXContext pxContext, PXGraphEventSemanticModel graphOrGraphExt)
2930
{
3031
context.CancellationToken.ThrowIfCancellationRequested();
31-
3232
CheckIfGraphOrGraphExtensionIsGenericNonAbstract(context, pxContext, graphOrGraphExt);
33+
34+
context.CancellationToken.ThrowIfCancellationRequested();
35+
CheckIfGraphOrGraphExtensionIsSealed(context, pxContext, graphOrGraphExt);
3336
}
3437

3538
protected virtual void CheckIfGraphOrGraphExtensionIsGenericNonAbstract(SymbolAnalysisContext context, PXContext pxContext,
3639
PXGraphEventSemanticModel graphOrGraphExt)
3740
{
3841
if (graphOrGraphExt.Symbol.IsAbstract || !graphOrGraphExt.Symbol.IsGenericType ||
39-
graphOrGraphExt.Symbol.TypeParameters.IsDefaultOrEmpty || graphOrGraphExt.Node == null)
42+
graphOrGraphExt.Symbol.TypeParameters.IsDefaultOrEmpty)
4043
{
4144
return;
4245
}
4346

44-
var location = graphOrGraphExt.Node.Identifier.GetLocation().NullIfLocationKindIsNone() ??
47+
var location = graphOrGraphExt.Node!.Identifier.GetLocation().NullIfLocationKindIsNone() ??
4548
graphOrGraphExt.Node.GetLocation();
4649
var diagnostic = Diagnostic.Create(Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract, location,
4750
graphOrGraphExt.Symbol.Name);
4851

4952
context.ReportDiagnosticWithSuppressionCheck(diagnostic, pxContext.CodeAnalysisSettings);
5053
}
54+
55+
protected virtual void CheckIfGraphOrGraphExtensionIsSealed(SymbolAnalysisContext context, PXContext pxContext,
56+
PXGraphEventSemanticModel graphOrGraphExt)
57+
{
58+
if (!graphOrGraphExt.Symbol.IsSealed)
59+
return;
60+
61+
var sealedToken = graphOrGraphExt.Node!.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.SealedKeyword));
62+
var location = sealedToken != default
63+
? sealedToken.GetLocation().NullIfLocationKindIsNone()
64+
: null;
65+
66+
location ??= graphOrGraphExt.Node.Identifier.GetLocation().NullIfLocationKindIsNone() ??
67+
graphOrGraphExt.Node.GetLocation();
68+
var diagnostic = Diagnostic.Create(Descriptors.PX1113_SealedGraphsAndGraphExtensions, location, graphOrGraphExt.Symbol.Name);
69+
70+
context.ReportDiagnosticWithSuppressionCheck(diagnostic, pxContext.CodeAnalysisSettings);
71+
}
5172
}
5273
}

src/Acuminator/Acuminator.Analyzers/StaticAnalysis/DeclarationAnalysisGraphAndDac/TypeModifiersFix.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,38 @@ public class TypeModifiersFix : PXCodeFixProvider
2828
new HashSet<string>
2929
{
3030
Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract.Id,
31+
Descriptors.PX1113_SealedGraphsAndGraphExtensions.Id
3132
}
3233
.ToImmutableArray();
3334

3435
protected override Task RegisterCodeFixesForDiagnosticAsync(CodeFixContext context, Diagnostic diagnostic)
3536
{
36-
bool removeSealedModifier, addAbstractModifier;
37+
bool addAbstractModifier;
3738
string codeActionName;
3839

3940
if (diagnostic.Id == Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract.Id)
4041
{
41-
addAbstractModifier = true;
42-
removeSealedModifier = true;
43-
codeActionName = nameof(Resources.PX1112Fix).GetLocalized().ToString();
42+
addAbstractModifier = true;
43+
codeActionName = nameof(Resources.PX1112Fix).GetLocalized().ToString();
4444
}
45-
else
45+
else if (diagnostic.Id == Descriptors.PX1113_SealedGraphsAndGraphExtensions.Id)
4646
{
47-
return Task.CompletedTask;
47+
addAbstractModifier = false;
48+
codeActionName = nameof(Resources.PX1113Fix).GetLocalized().ToString();
4849
}
50+
else
51+
return Task.CompletedTask;
4952

5053
context.CancellationToken.ThrowIfCancellationRequested();
5154

5255
var codeAction = CodeAction.Create(codeActionName,
53-
cToken => UpdateTypeModifiers(context.Document, context.Span, removeSealedModifier,
54-
addAbstractModifier, cToken),
56+
cToken => UpdateTypeModifiers(context.Document, context.Span, addAbstractModifier, cToken),
5557
equivalenceKey: codeActionName);
5658
context.RegisterCodeFix(codeAction, diagnostic);
5759
return Task.CompletedTask;
5860
}
5961

60-
private async Task<Solution> UpdateTypeModifiers(Document document, TextSpan span, bool removeSealedModifier, bool addAbstractModifier,
62+
private async Task<Solution> UpdateTypeModifiers(Document document, TextSpan span, bool addAbstractModifier,
6163
CancellationToken cancellationToken)
6264
{
6365
Solution originalSolution = document.Project.Solution;
@@ -84,22 +86,22 @@ private async Task<Solution> UpdateTypeModifiers(Document document, TextSpan spa
8486
if (typeDeclarations.Length <= 1)
8587
{
8688
var changedSolutionForNonPartialType = UpdateTypeNodeModifiersForNonPartialType(document, root, graphOrDacOrExtensionToMakePublicNode,
87-
removeSealedModifier, addAbstractModifier);
89+
addAbstractModifier);
8890
return changedSolutionForNonPartialType;
8991
}
9092
else
9193
{
9294
var changedSolutionForPartialType = await UpdateTypeNodeModifiersForPartialType(document.Project.Solution, typeDeclarations,
93-
removeSealedModifier, addAbstractModifier, cancellationToken)
95+
addAbstractModifier, cancellationToken)
9496
.ConfigureAwait(false);
9597
return changedSolutionForPartialType;
9698
}
9799
}
98100

99101
private Solution UpdateTypeNodeModifiersForNonPartialType(Document document, SyntaxNode root, ClassDeclarationSyntax graphOrGraphExtTypeNode,
100-
bool removeSealedModifier, bool addAbstractModifier)
102+
bool addAbstractModifier)
101103
{
102-
var modifiedGraphOrGraphExtTypeNode = UpdateTypeNodeModifiers(graphOrGraphExtTypeNode, removeSealedModifier, addAbstractModifier);
104+
var modifiedGraphOrGraphExtTypeNode = UpdateTypeNodeModifiers(graphOrGraphExtTypeNode, addAbstractModifier);
103105

104106
if (ReferenceEquals(modifiedGraphOrGraphExtTypeNode, graphOrGraphExtTypeNode))
105107
return document.Project.Solution;
@@ -111,8 +113,7 @@ private Solution UpdateTypeNodeModifiersForNonPartialType(Document document, Syn
111113
}
112114

113115
private async Task<Solution> UpdateTypeNodeModifiersForPartialType(Solution originalSolution, ImmutableArray<SyntaxReference> graphOrGraphExtDeclarations,
114-
bool removeSealedModifier, bool addAbstractModifier,
115-
CancellationToken cancellation)
116+
bool addAbstractModifier, CancellationToken cancellation)
116117
{
117118
var solutionEditor = new SolutionEditor(originalSolution);
118119
var documentEditors = await GetAllDocumentEditorsAsync(solutionEditor, graphOrGraphExtDeclarations, cancellation).ConfigureAwait(false);
@@ -129,7 +130,7 @@ private async Task<Solution> UpdateTypeNodeModifiersForPartialType(Solution orig
129130

130131
foreach (var graphOrGraphExtTypeNode in graphOrGraphExtTypeNodesInDocument)
131132
{
132-
var modifiedGraphOrGraphExtTypeNode = UpdateTypeNodeModifiers(graphOrGraphExtTypeNode, removeSealedModifier, addAbstractModifier);
133+
var modifiedGraphOrGraphExtTypeNode = UpdateTypeNodeModifiers(graphOrGraphExtTypeNode, addAbstractModifier);
133134

134135
if (ReferenceEquals(modifiedGraphOrGraphExtTypeNode, graphOrGraphExtTypeNode))
135136
continue;
@@ -178,14 +179,13 @@ select typeDecl.GetSyntax(cancellation))
178179
.OfType<ClassDeclarationSyntax>();
179180
}
180181

181-
private ClassDeclarationSyntax UpdateTypeNodeModifiers(ClassDeclarationSyntax graphOrGraphExtensionNode,
182-
bool removeSealedModifier, bool addAbstractModifier)
182+
private ClassDeclarationSyntax UpdateTypeNodeModifiers(ClassDeclarationSyntax graphOrGraphExtensionNode, bool addAbstractModifier)
183183
{
184184
var oldModifiers = graphOrGraphExtensionNode.Modifiers;
185185
SyntaxTokenList newModifiers = oldModifiers;
186186
bool modifiedAny = false;
187187

188-
if (removeSealedModifier && oldModifiers.Count > 0)
188+
if (oldModifiers.Count > 0)
189189
{
190190
int sealedIndex = newModifiers.IndexOf(SyntaxKind.SealedKeyword);
191191
if (sealedIndex >= 0 && sealedIndex < newModifiers.Count)

src/Acuminator/Acuminator.Analyzers/StaticAnalysis/Descriptors.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,5 +541,8 @@ private static DiagnosticDescriptor Rule(string id, LocalizableString title, Cat
541541

542542
public static DiagnosticDescriptor PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract { get; } =
543543
Rule("PX1112", nameof(Resources.PX1112Title).GetLocalized(), Category.Acuminator, DiagnosticSeverity.Error, DiagnosticsShortName.PX1112);
544+
545+
public static DiagnosticDescriptor PX1113_SealedGraphsAndGraphExtensions { get; } =
546+
Rule("PX1113", nameof(Resources.PX1113Title).GetLocalized(), Category.Acuminator, DiagnosticSeverity.Warning, DiagnosticsShortName.PX1113);
544547
}
545548
}

src/Acuminator/Acuminator.Tests/Tests/StaticAnalysis/DeclarationAnalysisGraphAndDac/GenericNonAbstractGraphsAndGraphExtensionsTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Microsoft.CodeAnalysis.Diagnostics;
1616

1717
using Xunit;
18+
using Acuminator.Utilities.Roslyn.Semantic;
19+
using Acuminator.Utilities.Roslyn.Semantic.PXGraph;
1820

1921
namespace Acuminator.Tests.Tests.StaticAnalysis.DeclarationAnalysisGraphAndDac
2022
{
@@ -91,6 +93,10 @@ private sealed class GraphAndGraphExtensionDeclarationAnalyzerForPX1112Tests : G
9193
(
9294
Descriptors.PX1112_GenericGraphsAndGraphExtensionsMustBeAbstract
9395
);
96+
97+
protected override void CheckIfGraphOrGraphExtensionIsSealed(SymbolAnalysisContext context, PXContext pxContext,
98+
PXGraphEventSemanticModel graphOrGraphExt)
99+
{ }
94100
}
95101
}
96102
}

0 commit comments

Comments
 (0)