Skip to content

Commit 2cb233a

Browse files
Merge pull request #17 from jonathanalgar/best-practices-statelessness
Beyond the rules: info-level guidance for best practices > statelessness checks
2 parents c0c0280 + ca525f5 commit 2cb233a

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/CustomCode-Analyzer/Analyzer.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static class DiagnosticIds
4141
public const string MissingStructureDecoration = "MissingStructureDecoration";
4242
public const string UnsupportedParameterType = "UnsupportedParameterType";
4343
public const string UnsupportedDefaultValue = "UnsupportedDefaultValue";
44+
public const string PotentialStatefulImplementation = "PotentialStatefulImplementation";
4445
}
4546

4647
/// <summary>
@@ -292,6 +293,18 @@ public static class Categories
292293

293294
// https://www.outsystems.com/tk/redirect?g=OS-ELG-MODL-05029 - not implementing
294295

296+
// ----------------------------------------------- BEST PRACTICES
297+
298+
private static readonly DiagnosticDescriptor PotentialStatefulImplementationRule = new(
299+
DiagnosticIds.PotentialStatefulImplementation,
300+
title: "Possible stateful behavior",
301+
messageFormat: "The class '{0}' contains static members ({1}) which could persist state between calls. External libraries should be designed to be stateless.",
302+
category: Categories.Design,
303+
defaultSeverity: DiagnosticSeverity.Info,
304+
isEnabledByDefault: true,
305+
description: "External libraries should be designed to be stateless. Consider passing state information as method parameters instead of storing it in fields.",
306+
helpLinkUri: "https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/extend_your_apps_with_custom_code/external_libraries_sdk_readme/#architecture");
307+
295308
/// <summary>
296309
/// Returns the full set of DiagnosticDescriptors that this analyzer is capable of producing.
297310
/// </summary>
@@ -318,7 +331,8 @@ public static class Categories
318331
UnsupportedTypeMappingRule,
319332
MissingStructureDecorationRule,
320333
UnsupportedParameterTypeRule,
321-
UnsupportedDefaultValueRule);
334+
UnsupportedDefaultValueRule,
335+
PotentialStatefulImplementationRule);
322336

323337
/// <summary>
324338
/// Entry point for the analyzer. Initializes analysis by setting up compilation-level
@@ -777,6 +791,27 @@ private static void AnalyzeClass(SymbolAnalysisContext context, INamedTypeSymbol
777791
ctorDecl.Identifier.GetLocation(),
778792
typeSymbol.Name));
779793
}
794+
795+
// Check for static members that could indicate attempts to maintain state
796+
var staticMembers = typeSymbol.GetMembers()
797+
.Where(member =>
798+
member.IsStatic &&
799+
!member.IsImplicitlyDeclared && // Skip compiler-generated members
800+
member is IFieldSymbol { IsConst: false } or IPropertySymbol)
801+
.Select(m => m.Name)
802+
.OrderBy(name => name)
803+
.ToList();
804+
805+
if (staticMembers.Any() &&
806+
typeSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is ClassDeclarationSyntax stateDecl)
807+
{
808+
context.ReportDiagnostic(
809+
Diagnostic.Create(
810+
PotentialStatefulImplementationRule,
811+
stateDecl.Identifier.GetLocation(),
812+
typeSymbol.Name,
813+
string.Join(", ", staticMembers)));
814+
}
780815
}
781816
}
782817
}

tests/CustomCode-Analyzer.Tests/AnalyzerTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,51 @@ public void TestMethod(string text1 = Constants.DefaultText,
13381338

13391339
// https://www.outsystems.com/tk/redirect?g=OS-ELG-MODL-05029 - not implementing
13401340

1341+
// ----------------------------------------------- BEST PRACTICES
1342+
1343+
// --------------------- PotentialStatefulImplementationRule ---------------
1344+
[TestMethod]
1345+
public async Task PotentialStatefulImplementationRule_DetectsStaticState()
1346+
{
1347+
var test = @"
1348+
using System.Collections.Generic;
1349+
1350+
[OSInterface]
1351+
public interface ICalculator
1352+
{
1353+
decimal Add(decimal a, decimal b);
1354+
int GetTotalCalculations();
1355+
}
1356+
1357+
public class Calculator : ICalculator
1358+
{
1359+
// Attempting to track state between calls
1360+
private static int _totalCalculations;
1361+
private static readonly List<decimal> _history = new();
1362+
public static decimal LastResult { get; private set; }
1363+
1364+
public decimal Add(decimal a, decimal b)
1365+
{
1366+
_totalCalculations++;
1367+
var result = a + b;
1368+
_history.Add(result);
1369+
LastResult = result;
1370+
return result;
1371+
}
1372+
1373+
public int GetTotalCalculations()
1374+
{
1375+
return _totalCalculations;
1376+
}
1377+
}";
1378+
1379+
var expected = CSharpAnalyzerVerifier<Analyzer>
1380+
.Diagnostic(DiagnosticIds.PotentialStatefulImplementation)
1381+
.WithSpan(11, 14, 11, 24)
1382+
.WithArguments("Calculator", "_history, _totalCalculations, LastResult");
1383+
1384+
await CSharpAnalyzerVerifier<Analyzer>.VerifyAnalyzerAsync(test, TestContext, skipSDKreference: false, expected);
1385+
}
13411386
// ----------------------------------------------- MIXED TESTS!
13421387
[TestMethod]
13431388
public async Task ComplexScenario_MultipleNamingAndStructureIssues_ReportsWarnings()

0 commit comments

Comments
 (0)