diff --git a/src/D2L.CodeStyle.Analyzers/ApiUsage/ConstantAttributeAnalyzer.cs b/src/D2L.CodeStyle.Analyzers/ApiUsage/ConstantAttributeAnalyzer.cs index 79490eae..ec60be3e 100644 --- a/src/D2L.CodeStyle.Analyzers/ApiUsage/ConstantAttributeAnalyzer.cs +++ b/src/D2L.CodeStyle.Analyzers/ApiUsage/ConstantAttributeAnalyzer.cs @@ -14,7 +14,8 @@ public sealed class ConstantAttributeAnalyzer : DiagnosticAnalyzer { ImmutableArray.Create( Diagnostics.NonConstantPassedToConstantParameter, Diagnostics.InvalidConstantType, - Diagnostics.ReferenceToMethodWithConstantParameterNotSupport + Diagnostics.ReferenceToMethodWithConstantParameterNotSupport, + Diagnostics.UnexpectedNumberOfParametersForImplicitOperator ); public override void Initialize( AnalysisContext context ) { @@ -132,6 +133,16 @@ ISymbol constantAttribute return; } + // Argument is inside an expression tree (e.g. Moq Verify/Setup), so do nothing + if( IsInsideExpressionTree( argument ) ) { + return; + } + + // Argument is a mock argument constraint (e.g. Arg.Is.Anything), so do nothing + if( IsMockArgumentConstraint( argument.Value ) ) { + return; + } + // Argument was defined as [Constant] already, so trust it var argumentSymbol = argument.SemanticModel.GetSymbolInfo( argument.Value.Syntax, context.CancellationToken ).Symbol; if( argumentSymbol != null && HasAttribute( argumentSymbol, constantAttribute ) ) { @@ -288,5 +299,59 @@ private static bool TypeCanBeConstant( SpecialType specialType ) { return false; } } + + private static bool IsInsideExpressionTree( + IOperation operation + ) { + IOperation current = operation; + while( current != null ) { + if( current is IConversionOperation conversion + && conversion.Type is INamedTypeSymbol namedType + && namedType.BaseType != null + && namedType.BaseType.Name == "LambdaExpression" + ) { + return true; + } + current = current.Parent; + } + return false; + } + + private static bool IsMockArgumentConstraint( IOperation operation ) { + // Matches patterns like Arg.Is.Anything or Arg.Is.Equal(...) (Rhino Mocks) + // Arg.Is.Anything is a property chain: Arg.Is (static) -> .Anything (instance) + // Arg.Is.Equal(...) is a method call on the .Is property result + if( operation is IInvocationOperation invocation ) { + var containingType = invocation.TargetMethod.ContainingType; + if( IsArgType( containingType ) ) { + return true; + } + // Check if the instance receiver is an Arg property chain + if( invocation.Instance != null && IsMockArgumentConstraint( invocation.Instance ) ) { + return true; + } + } + + IOperation current = operation; + while( current is IPropertyReferenceOperation propertyRef ) { + var containingType = propertyRef.Property.ContainingType; + if( IsArgType( containingType ) ) { + return true; + } + current = propertyRef.Instance; + } + return false; + } + + private static bool IsArgType( INamedTypeSymbol type ) { + if( type == null ) { + return false; + } + if( type.Name == "Arg" && type.IsGenericType ) { + return true; + } + // Check containing types for nested types within Arg + return IsArgType( type.ContainingType ); + } } } diff --git a/src/D2L.CodeStyle.TestAnalyzers/ApiUsage/ConstantAttributeAnalyzer.cs b/src/D2L.CodeStyle.TestAnalyzers/ApiUsage/ConstantAttributeAnalyzer.cs new file mode 100644 index 00000000..7317cd6e --- /dev/null +++ b/src/D2L.CodeStyle.TestAnalyzers/ApiUsage/ConstantAttributeAnalyzer.cs @@ -0,0 +1,369 @@ +#nullable disable + +using System.Collections.Immutable; +using D2L.CodeStyle.TestAnalyzers.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace D2L.CodeStyle.TestAnalyzers.ApiUsage { + [DiagnosticAnalyzer( LanguageNames.CSharp )] + public sealed class ConstantAttributeAnalyzer : DiagnosticAnalyzer { + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create( + Diagnostics.NonConstantPassedToConstantParameter, + Diagnostics.InvalidConstantType, + Diagnostics.ReferenceToMethodWithConstantParameterNotSupport, + Diagnostics.UnexpectedNumberOfParametersForImplicitOperator + ); + + public override void Initialize( AnalysisContext context ) { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics ); + context.RegisterCompilationStartAction( CompilationStart ); + } + + public static void CompilationStart( + CompilationStartAnalysisContext context + ) { + var constantAttribute = context.Compilation.GetTypeByMetadataName( + "D2L.CodeStyle.Annotations.Contract.ConstantAttribute" + ); + + // The D2L.CodeStyle.Annotations reference is optional + if( constantAttribute == null || constantAttribute.Kind == SymbolKind.ErrorType ) { + return; + } + + context.RegisterSymbolAction( + ctx => AnalyzeParameter( + ctx, + (IParameterSymbol)ctx.Symbol, + constantAttribute + ), + SymbolKind.Parameter + ); + + context.RegisterOperationAction( + ctx => AnalyzeArgument( + ctx, + (IArgumentOperation)ctx.Operation, + constantAttribute + ), + OperationKind.Argument + ); + + context.RegisterOperationAction( + ctx => AnalyzeConversion( + ctx, + (IConversionOperation)ctx.Operation, + constantAttribute + ), + OperationKind.Conversion + ); + + context.RegisterOperationAction( + ctx => AnalyzeCompoundAssignment( + ctx, + (ICompoundAssignmentOperation)ctx.Operation, + constantAttribute + ), + OperationKind.CompoundAssignment + ); + + context.RegisterOperationAction( + ctx => AnalyzeMethodReference( + ctx, + (IMethodReferenceOperation)ctx.Operation, + constantAttribute + ), + OperationKind.MethodReference + ); + } + + private static void AnalyzeParameter( + SymbolAnalysisContext context, + IParameterSymbol parameter, + ISymbol constantAttribute + ) { + // Parameter is not [Constant], so do nothing + if( !HasAttribute( parameter, constantAttribute ) ) { + return; + } + + var type = parameter.Type; + + // Special types (bool, enum, int, string, etc) are the only types + // which might be constant, aside from type parameters filled with + // special types + if( TypeCanBeConstant( type.SpecialType ) ) { + return; + } + + // If the parameter's type is a type parameter, then whether + // it can be constant depends on the type that gets filled; + // We can't determine that here + if( type.Kind == SymbolKind.TypeParameter ) { + return; + } + + // The current parameter type cannot be marked as [Constant] + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.InvalidConstantType, + location: parameter.Locations.First(), + messageArgs: new object[] { type.TypeKind } + ) + ); + } + + private static void AnalyzeArgument( + OperationAnalysisContext context, + IArgumentOperation argument, + ISymbol constantAttribute + ) { + var parameter = argument.Parameter; + + // Parameter is not [Constant], so do nothing + if( !HasAttribute( parameter, constantAttribute ) ) { + return; + } + + // Argument is a constant value or string.Empty, so do nothing + if( argument.Value.ConstantValue.HasValue || IsStringEmpty( argument.Value ) ) { + return; + } + + // Argument is inside an expression tree (e.g. Moq Verify/Setup), so do nothing + if( IsInsideExpressionTree( argument ) ) { + return; + } + + // Argument is a mock argument constraint (e.g. Arg.Is.Anything), so do nothing + if( IsMockArgumentConstraint( argument.Value ) ) { + return; + } + + // Argument was defined as [Constant] already, so trust it + var argumentSymbol = argument.SemanticModel.GetSymbolInfo( argument.Value.Syntax, context.CancellationToken ).Symbol; + if( argumentSymbol != null && HasAttribute( argumentSymbol, constantAttribute ) ) { + return; + } + + // Argument is not constant, so report it + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.NonConstantPassedToConstantParameter, + location: argument.Syntax.GetLocation(), + messageArgs: new[] { parameter.Name } + ) + ); + } + + private static void AnalyzeConversion( + OperationAnalysisContext context, + IConversionOperation conversion, + INamedTypeSymbol constantAttribute + ) { + + IMethodSymbol @operator = conversion.OperatorMethod; + if( @operator is null ) { + return; + } + if( @operator.Parameters.Length != 1 ) { + return; + } + + // Operator parameter is not [Constant], so do nothing + IParameterSymbol parameter = @operator.Parameters[0]; + if( !HasAttribute( parameter, constantAttribute ) ) { + return; + } + + // Operand is a constant value or string.Empty, so trust it + IOperation operand = conversion.Operand; + if( operand.ConstantValue.HasValue || IsStringEmpty( operand ) ) { + return; + } + + // Operand was defined as [Constant] already, so trust it + ISymbol operandSymbol = conversion.SemanticModel.GetSymbolInfo( operand.Syntax ).Symbol; + if( operandSymbol is not null && HasAttribute( operandSymbol, constantAttribute ) ) { + return; + } + + // Operand is not constant, so report it + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.NonConstantPassedToConstantParameter, + location: operand.Syntax.GetLocation(), + messageArgs: new[] { parameter.Name } + ) + ); + } + + private static void AnalyzeCompoundAssignment( + OperationAnalysisContext context, + ICompoundAssignmentOperation compoundAssignment, + INamedTypeSymbol constantAttribute + ) { + + // Get implicit operator for the compound assignment + IMethodSymbol @operator = compoundAssignment.OutConversion.MethodSymbol; + if( @operator is null ) { + return; + } + if( @operator.Parameters.Length != 1 ) { + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.UnexpectedNumberOfParametersForImplicitOperator, + location: compoundAssignment.Value.Syntax.GetLocation() + ) + ); + return; + } + + // Operator parameter is not [Constant], so do nothing + IParameterSymbol parameter = @operator.Parameters[0]; + if( !HasAttribute( parameter, constantAttribute ) ) { + return; + } + + // Compound assignment with operator parameter that has [Constant] cannot be constant, so report it + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.NonConstantPassedToConstantParameter, + location: compoundAssignment.Value.Syntax.GetLocation(), + messageArgs: new[] { parameter.Name } + ) + ); + } + + private static void AnalyzeMethodReference( + OperationAnalysisContext context, + IMethodReferenceOperation operation, + INamedTypeSymbol constantAttribute + ) { + + foreach( IParameterSymbol parameter in operation.Method.Parameters ) { + + if( !HasAttribute( parameter, constantAttribute ) ) { + continue; + } + + context.ReportDiagnostic( + Diagnostic.Create( + descriptor: Diagnostics.ReferenceToMethodWithConstantParameterNotSupport, + location: operation.Syntax.GetLocation() + ) + ); + + return; + } + } + + /// + /// Check if the symbol has a specific attribute attached to it. + /// + /// The symbol to check for an attribute on + /// The symbol of the attribute + /// True if the attribute exists on the symbol, false otherwise + private static bool HasAttribute( + ISymbol symbol, + ISymbol attributeSymbol + ) { + return symbol.GetAttributes() + .Any( attr => SymbolEqualityComparer.Default.Equals( + attributeSymbol, + attr.AttributeClass + ) + ); + } + + private static bool IsStringEmpty( IOperation operation ) { + if( operation is IFieldReferenceOperation fieldRef ) { + return fieldRef.Field.ContainingType.SpecialType == SpecialType.System_String + && fieldRef.Field.Name == "Empty"; + } + return false; + } + + private static bool TypeCanBeConstant( SpecialType specialType ) { + switch( specialType ) { + case SpecialType.System_Enum: + case SpecialType.System_Boolean: + case SpecialType.System_Char: + case SpecialType.System_SByte: + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Decimal: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_String: + return true; + default: + return false; + } + } + + private static bool IsInsideExpressionTree( + IOperation operation + ) { + IOperation current = operation; + while( current != null ) { + if( current is IConversionOperation conversion + && conversion.Type is INamedTypeSymbol namedType + && namedType.BaseType != null + && namedType.BaseType.Name == "LambdaExpression" + ) { + return true; + } + current = current.Parent; + } + return false; + } + + private static bool IsMockArgumentConstraint( IOperation operation ) { + // Matches patterns like Arg.Is.Anything or Arg.Is.Equal(...) (Rhino Mocks) + // Arg.Is.Anything is a property chain: Arg.Is (static) -> .Anything (instance) + // Arg.Is.Equal(...) is a method call on the .Is property result + if( operation is IInvocationOperation invocation ) { + var containingType = invocation.TargetMethod.ContainingType; + if( IsArgType( containingType ) ) { + return true; + } + // Check if the instance receiver is an Arg property chain + if( invocation.Instance != null && IsMockArgumentConstraint( invocation.Instance ) ) { + return true; + } + } + + IOperation current = operation; + while( current is IPropertyReferenceOperation propertyRef ) { + var containingType = propertyRef.Property.ContainingType; + if( IsArgType( containingType ) ) { + return true; + } + current = propertyRef.Instance; + } + return false; + } + + private static bool IsArgType( INamedTypeSymbol type ) { + if( type == null ) { + return false; + } + if( type.Name == "Arg" && type.IsGenericType ) { + return true; + } + // Check containing types for nested types within Arg + return IsArgType( type.ContainingType ); + } + } +} diff --git a/src/D2L.CodeStyle.TestAnalyzers/Common/Diagnostics.cs b/src/D2L.CodeStyle.TestAnalyzers/Common/Diagnostics.cs index 13e648da..9a3435b4 100644 --- a/src/D2L.CodeStyle.TestAnalyzers/Common/Diagnostics.cs +++ b/src/D2L.CodeStyle.TestAnalyzers/Common/Diagnostics.cs @@ -51,5 +51,42 @@ public static class Diagnostics { description: "Unnecessarily listed in an analyzer allowed list.", customTags: new[] { "CompilationEnd" } ); - } + + public static readonly DiagnosticDescriptor NonConstantPassedToConstantParameter = new DiagnosticDescriptor( + id: "D2L0074", + title: "Constant parameter cannot be passed a non-constant value", + messageFormat: "The \"{0}\" parameter is marked with the [Constant] attribute, and so it must be passed a compile-time constant value", + category: "Safety", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The method being called has declared that this parameter must receive a constant, but a non-constant value is being passed." + ); + + public static readonly DiagnosticDescriptor InvalidConstantType = new DiagnosticDescriptor( + id: "D2L0075", + title: "Invalid data type marked as [Constant]", + messageFormat: "The [Constant] attribute cannot be used on \"{0}\" types, because they cannot be compile-time constants", + category: "Correctness", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor ReferenceToMethodWithConstantParameterNotSupport = new DiagnosticDescriptor( + id: "D2L0102", + title: "References to methods with parameters marked as [Constant] is not supported", + messageFormat: "References to methods with parameters marked as [Constant] is currently not supported", + category: "Correctness", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public static readonly DiagnosticDescriptor UnexpectedNumberOfParametersForImplicitOperator = new DiagnosticDescriptor( + id: "D2L0104", + title: "Unexpected number of parameters for implicit operator", + messageFormat: "The implicit operator has an unexpected number of parameters. Update analyzer to handle this case.", + category: "Safety", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + } } diff --git a/tests/D2L.CodeStyle.TestAnalyzers.Tests/D2L.CodeStyle.TestAnalyzers.Tests.csproj b/tests/D2L.CodeStyle.TestAnalyzers.Tests/D2L.CodeStyle.TestAnalyzers.Tests.csproj index 1b993201..2f6adfb4 100644 --- a/tests/D2L.CodeStyle.TestAnalyzers.Tests/D2L.CodeStyle.TestAnalyzers.Tests.csproj +++ b/tests/D2L.CodeStyle.TestAnalyzers.Tests/D2L.CodeStyle.TestAnalyzers.Tests.csproj @@ -12,7 +12,14 @@ + + + + + + + diff --git a/tests/D2L.CodeStyle.TestAnalyzers.Tests/Spec.cs b/tests/D2L.CodeStyle.TestAnalyzers.Tests/Spec.cs new file mode 100644 index 00000000..16be4d76 --- /dev/null +++ b/tests/D2L.CodeStyle.TestAnalyzers.Tests/Spec.cs @@ -0,0 +1,115 @@ +using System.Collections.Immutable; +using System.Reflection; +using System.Text.RegularExpressions; +using D2L.CodeStyle.Annotations; +using D2L.CodeStyle.TestAnalyzers.Common; +using D2L.CodeStyle.SpecTests; +using Microsoft.CodeAnalysis; +using NUnit.Framework; + +namespace D2L.CodeStyle.TestAnalyzers { + + [TestFixtureSource( nameof( GetSpecTests ) )] + internal sealed class Spec : SpecTestFixtureBase { + + /// + /// Parameterized test fixture of . + /// + /// Tests returned by . + public Spec( SpecTest test ) : base( test ) { } + + private static IEnumerable GetSpecTests() { + + ImmutableArray additionalFiles = GetAdditionalFiles(); + ImmutableDictionary diagnosticDescriptors = GetDiagnosticDescriptors(); + ImmutableArray metdataReferences = GetMetadataReferences(); + + foreach( (string name, string source) in GetEmbeddedSpecTests() ) { + + yield return new SpecTest( + Name: name, + Source: source, + AdditionalFiles: additionalFiles, + MetadataReferences: metdataReferences, + DiagnosticDescriptors: diagnosticDescriptors + ); + } + } + + private static ImmutableDictionary GetDiagnosticDescriptors() { + + var builder = ImmutableDictionary.CreateBuilder(); + + foreach( FieldInfo field in typeof( Diagnostics ).GetFields() ) { + + if( field.GetValue( null ) is DiagnosticDescriptor diagnosticDescriptor ) { + builder.Add( field.Name, diagnosticDescriptor ); + } + } + + return builder.ToImmutable(); + } + + private static ImmutableArray GetMetadataReferences() { + + var builder = ImmutableArray.CreateBuilder(); + + builder.AddAssemblyOf( typeof( object ) ); // mscorlib + builder.AddAssemblyOf( typeof( Uri ) ); // System + builder.AddAssemblyOf( typeof( Enumerable ) ); // System.Core + builder.AddAssemblyOf( typeof( Annotations.Because ) ); // D2L.CodeStyle.Annotations + + builder.AddAssembly( "System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" ); + + return builder.ToImmutable(); + } + + private static ImmutableArray GetAdditionalFiles() { + + var builder = ImmutableArray.CreateBuilder(); + + Assembly testAssembly = Assembly.GetExecutingAssembly(); + foreach( string resourcePath in testAssembly.GetManifestResourceNames() ) { + + if( !resourcePath.EndsWith( "AllowedList.txt" ) ) { + continue; + } + + string virtualPath = Regex.Replace( + resourcePath, + @"^.*\.(?[^\.]*)\.txt$", + @"${allowedListName}.txt" + ); + + string text = testAssembly.ReadEmbeddedResourceAsString( resourcePath ); + + builder.Add( new AdditionalFile( virtualPath, text ) ); + } + + return builder.ToImmutable(); + } + + private static IEnumerable<(string SpecName, string Source)> GetEmbeddedSpecTests() { + + Assembly assembly = Assembly.GetExecutingAssembly(); + + foreach( string specFilePath in assembly.GetManifestResourceNames() ) { + + if( !specFilePath.EndsWith( ".cs" ) ) { + continue; + } + + // The file foo/bar.baz.cs has specName bar.baz + string specName = Regex.Replace( + specFilePath, + @"^.*\.(?[^\.]*)\.cs$", + @"${specName}" + ); + + string source = assembly.ReadEmbeddedResourceAsString( specFilePath ); + + yield return (specName, source); + } + } + } +} diff --git a/tests/D2L.CodeStyle.TestAnalyzers.Tests/Specs/ConstantAttributeAnalyzer.cs b/tests/D2L.CodeStyle.TestAnalyzers.Tests/Specs/ConstantAttributeAnalyzer.cs new file mode 100644 index 00000000..255ed3af --- /dev/null +++ b/tests/D2L.CodeStyle.TestAnalyzers.Tests/Specs/ConstantAttributeAnalyzer.cs @@ -0,0 +1,335 @@ +// analyzer: D2L.CodeStyle.TestAnalyzers.ApiUsage.ConstantAttributeAnalyzer, D2L.CodeStyle.TestAnalyzers + +using System; + +namespace SpecTests { + + using D2L.CodeStyle.Annotations.Contract; + + public sealed class Logger { + public static void Error( [Constant] string message ) { } + } + + public sealed class WrappedLogger { + public static void Error( [Constant] string message ) { + Logger.Error( message ); + } + public static void OtherError( string message ) { + Logger.Error( /* NonConstantPassedToConstantParameter(message) */ message /**/ ); + } + } + + public sealed class Types { + + public static void SomeMethodWithConstantParameter( [Constant] T param1 ) { } + public static void SomeMethodWithParameter( T param1 ) { } + public static void SomeMethodWithOneConstantParameter( [Constant] T param1, T param2 ) { } + public static void SomeMethodWithOneOtherConstantParameter( T param1, [Constant] T param2 ) { } + public static void SomeMethodWithTwoConstantParameters( [Constant] T param1, [Constant] T param2 ) { } + + public interface IInterface { } + public class SomeClassImplementingInterface : IInterface { } + public static void SomeMethodWithInterfaceParameter( [Constant] IInterface /* InvalidConstantType(Interface) */ @interface /**/ ) { } + public static void SomeMethodWithOneInterfaceParameter( IInterface param1, [Constant] T param2 ) { } + + public readonly struct ConstantStruct { + public ConstantStruct( [Constant] string value ) { + Value = value; + } + public string Value { get; } + public static implicit operator ConstantStruct( [Constant] string value ) { + return new ConstantStruct( value ); + } + public static explicit operator ConstantStruct( [Constant] bool value ) { + return new ConstantStruct( "true" ); + } + } + + public readonly struct NonConstantStruct { + public NonConstantStruct( string value ) { + Value = value; + } + public string Value { get; } + public static implicit operator NonConstantStruct( string value ) { + return new NonConstantStruct( value ); + } + public static explicit operator NonConstantStruct( bool value ) { + return new NonConstantStruct( "true" ); + } + } + } + + public sealed class Tests { + + private static class Constants { + public const bool Bool = true; + public const string String = "foo"; + } + + void Method() { + + #region Invalid type tests + const Types.SomeClassImplementingInterface interfaceClass = new Types.SomeClassImplementingInterface { }; + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ interfaceClass /**/ ); + Types.SomeMethodWithOneInterfaceParameter( interfaceClass, /* NonConstantPassedToConstantParameter(param2) */ interfaceClass /**/ ); + #endregion + + #region Logger tests + const string CONSTANT_MESSAGE = "Organization {0} is not constant."; + int orgId = 0; + + Logger.Error( CONSTANT_MESSAGE ); + Logger.Error( "Organization 1 is constant." ); + Logger.Error( string.Empty ); + Logger.Error( /* NonConstantPassedToConstantParameter(message) */ string.Format(CONSTANT_MESSAGE, orgId) /**/ ); + Logger.Error( /* NonConstantPassedToConstantParameter(message) */ $"Organization {orgId} is not constant." /**/ ); + + const string CONSTANT_STRING = "Foo"; + Logger.Error( $"Interpolated strings of constant strings (like {CONSTANT_STRING}) are compile-time constants as of C#10." ); + #endregion + + #region String tests + string variableStr = "This is a variable message"; + const string CONSTANT_STR = "This is a constant message"; + + Types.SomeMethodWithConstantParameter( "This is a constant message" ); + Types.SomeMethodWithConstantParameter( CONSTANT_STR ); + Types.SomeMethodWithConstantParameter( CONSTANT_STR + "This is a constant message" ); + Types.SomeMethodWithConstantParameter( string.Empty ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ CONSTANT_STR + variableStr /**/ ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableStr /**/ ); + + Types.SomeMethodWithParameter( "This is a constant message" ); + Types.SomeMethodWithParameter( CONSTANT_STR ); + Types.SomeMethodWithParameter( CONSTANT_STR + "This is a constant message" ); + Types.SomeMethodWithConstantParameter( string.Empty ); + Types.SomeMethodWithParameter( CONSTANT_STR + variableStr ); + Types.SomeMethodWithParameter( variableStr ); + + Types.SomeMethodWithOneConstantParameter( "This is a constant message", "This is a constant message" ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_STR, CONSTANT_STR ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_STR + "This is a constant message", CONSTANT_STR + "This is a constant message" ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_STR, variableStr ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableStr /**/, CONSTANT_STR ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableStr /**/, variableStr ); + + Types.SomeMethodWithOneOtherConstantParameter( "This is a constant message", "This is a constant message" ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_STR, CONSTANT_STR ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_STR + "This is a constant message", CONSTANT_STR + "This is a constant message" ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_STR, /* NonConstantPassedToConstantParameter(param2) */ variableStr /**/ ); + Types.SomeMethodWithOneOtherConstantParameter( variableStr, CONSTANT_STR ); + Types.SomeMethodWithOneOtherConstantParameter( variableStr, /* NonConstantPassedToConstantParameter(param2) */ variableStr /**/ ); + + Types.SomeMethodWithTwoConstantParameters( "This is a constant message", "This is a constant message" ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_STR, CONSTANT_STR ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_STR + "This is a constant message", CONSTANT_STR + "This is a constant message" ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_STR, /* NonConstantPassedToConstantParameter(param2) */ variableStr /**/ ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableStr /**/, CONSTANT_STR ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableStr /**/, /* NonConstantPassedToConstantParameter(param2) */ variableStr /**/ ); + #endregion + + #region Number tests + int variableInt = 5; + const int CONSTANT_INT = 29; + + Types.SomeMethodWithConstantParameter( 29 ); + Types.SomeMethodWithConstantParameter( CONSTANT_INT ); + Types.SomeMethodWithConstantParameter( CONSTANT_INT + 29 ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ CONSTANT_INT + variableInt /**/ ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableInt /**/ ); + + Types.SomeMethodWithParameter( 29 ); + Types.SomeMethodWithParameter( CONSTANT_INT ); + Types.SomeMethodWithParameter( CONSTANT_INT + 29 ); + Types.SomeMethodWithParameter( CONSTANT_INT + variableInt ); + Types.SomeMethodWithParameter( variableInt ); + + Types.SomeMethodWithOneConstantParameter( 29, 29 ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_INT, CONSTANT_INT ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_INT + 29, CONSTANT_INT + 29 ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_INT, variableInt ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableInt /**/, CONSTANT_INT ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableInt /**/, variableInt ); + + Types.SomeMethodWithOneOtherConstantParameter( 29, 29 ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_INT, CONSTANT_INT ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_INT + 29, CONSTANT_INT + 29 ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_INT, /* NonConstantPassedToConstantParameter(param2) */ variableInt /**/ ); + Types.SomeMethodWithOneOtherConstantParameter( variableInt, CONSTANT_INT ); + Types.SomeMethodWithOneOtherConstantParameter( variableInt, /* NonConstantPassedToConstantParameter(param2) */ variableInt /**/ ); + + Types.SomeMethodWithTwoConstantParameters( 29, 29 ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_INT, CONSTANT_INT ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_INT + 29, CONSTANT_INT + 29 ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_INT, /* NonConstantPassedToConstantParameter(param2) */ variableInt /**/ ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableInt /**/, CONSTANT_INT ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableInt /**/, /* NonConstantPassedToConstantParameter(param2) */ variableInt /**/ ); + #endregion + + #region Boolean tests + bool variableBool = true; + const bool CONSTANT_BOOL = false; + + Types.SomeMethodWithConstantParameter( false ); + Types.SomeMethodWithConstantParameter( CONSTANT_BOOL ); + Types.SomeMethodWithConstantParameter( CONSTANT_BOOL || false ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ CONSTANT_BOOL || variableBool /**/ ); + Types.SomeMethodWithConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableBool /**/ ); + + Types.SomeMethodWithParameter( false ); + Types.SomeMethodWithParameter( CONSTANT_BOOL ); + Types.SomeMethodWithParameter( CONSTANT_BOOL || false ); + Types.SomeMethodWithParameter( CONSTANT_BOOL || variableBool ); + Types.SomeMethodWithParameter( variableBool ); + + Types.SomeMethodWithOneConstantParameter( false, false ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_BOOL, CONSTANT_BOOL ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_BOOL || false, CONSTANT_BOOL || false ); + Types.SomeMethodWithOneConstantParameter( CONSTANT_BOOL, variableBool ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableBool /**/, CONSTANT_BOOL ); + Types.SomeMethodWithOneConstantParameter( /* NonConstantPassedToConstantParameter(param1) */ variableBool /**/, variableBool ); + + Types.SomeMethodWithOneOtherConstantParameter( false, false ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_BOOL, CONSTANT_BOOL ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_BOOL || false, CONSTANT_BOOL || false ); + Types.SomeMethodWithOneOtherConstantParameter( CONSTANT_BOOL, /* NonConstantPassedToConstantParameter(param2) */ variableBool /**/ ); + Types.SomeMethodWithOneOtherConstantParameter( variableBool, CONSTANT_BOOL ); + Types.SomeMethodWithOneOtherConstantParameter( variableBool, /* NonConstantPassedToConstantParameter(param2) */ variableBool /**/ ); + + Types.SomeMethodWithTwoConstantParameters( false, false ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_BOOL, CONSTANT_BOOL ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_BOOL || false, CONSTANT_BOOL || false ); + Types.SomeMethodWithTwoConstantParameters( CONSTANT_BOOL, /* NonConstantPassedToConstantParameter(param2) */ variableBool /**/ ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableBool /**/, CONSTANT_BOOL ); + Types.SomeMethodWithTwoConstantParameters( /* NonConstantPassedToConstantParameter(param1) */ variableBool /**/, /* NonConstantPassedToConstantParameter(param2) */ variableBool /**/ ); + #endregion + } + + #region Constructor Tests + + void ConstructorTests( + [D2L.CodeStyle.Annotations.Contract.Constant] string trusted, + string untrusted + ) { + const string constant = "foo"; + string variable = "bar"; + + new Types.ConstantStruct( "abc" ); + new Types.ConstantStruct( string.Empty ); + new Types.ConstantStruct( Constants.String ); + new Types.ConstantStruct( constant ); + new Types.ConstantStruct( trusted ); + new Types.ConstantStruct( /* NonConstantPassedToConstantParameter(value) */ variable /**/ ); + new Types.ConstantStruct( /* NonConstantPassedToConstantParameter(value) */ untrusted /**/ ); + + new Types.NonConstantStruct( "abc" ); + new Types.NonConstantStruct( string.Empty ); + new Types.NonConstantStruct( Constants.String ); + new Types.NonConstantStruct( constant ); + new Types.NonConstantStruct( trusted ); + new Types.NonConstantStruct( variable ); + new Types.NonConstantStruct( untrusted ); + } + + #endregion + + #region Explicit Operator Tests + + void ExplicitOperatorTests( + [D2L.CodeStyle.Annotations.Contract.Constant] bool trusted, + bool untrusted + ) { + const bool constant = true; + bool variable = true; + + { Types.ConstantStruct v = (Types.ConstantStruct)true; } + { Types.ConstantStruct v = (Types.ConstantStruct)Constants.Bool; } + { Types.ConstantStruct v = (Types.ConstantStruct)constant; } + { Types.ConstantStruct v = (Types.ConstantStruct)trusted; } + { Types.ConstantStruct v = (Types.ConstantStruct) /* NonConstantPassedToConstantParameter(value) */ variable /**/; } + { Types.ConstantStruct v = (Types.ConstantStruct) /* NonConstantPassedToConstantParameter(value) */ untrusted /**/; } + + { Types.NonConstantStruct v = (Types.NonConstantStruct)true; } + { Types.NonConstantStruct v = (Types.NonConstantStruct)Constants.Bool; } + { Types.NonConstantStruct v = (Types.NonConstantStruct)constant; } + { Types.NonConstantStruct v = (Types.NonConstantStruct)trusted; } + { Types.NonConstantStruct v = (Types.NonConstantStruct)variable; } + { Types.NonConstantStruct v = (Types.NonConstantStruct)untrusted; } + } + + #endregion + + #region Implicit Operator Tests + + void ImplicitOperatorTests( + [D2L.CodeStyle.Annotations.Contract.Constant] string trusted, + string untrusted + ) { + const string constant = "foo"; + string variable = "bar"; + + { Types.ConstantStruct v = "abc"; } + { Types.ConstantStruct v = string.Empty; } + { Types.ConstantStruct v = Constants.String; } + { Types.ConstantStruct v = constant; } + { Types.ConstantStruct v = trusted; } + { Types.ConstantStruct v = /* NonConstantPassedToConstantParameter(value) */ variable /**/; } + { Types.ConstantStruct v = /* NonConstantPassedToConstantParameter(value) */ untrusted /**/; } + { + Types.ConstantStruct v = Constants.String; + v += /* NonConstantPassedToConstantParameter(value) */ "abc" /**/; + } + { + Types.ConstantStruct v = Constants.String; + v += /* NonConstantPassedToConstantParameter(value) */ trusted /**/; + } + { + Types.ConstantStruct v = Constants.String; + v += /* NonConstantPassedToConstantParameter(value) */ variable /**/; + } + { + Types.ConstantStruct v = Constants.String; + v += /* NonConstantPassedToConstantParameter(value) */ "a" + "b" /**/; + } + + { Types.NonConstantStruct v = "abc"; } + { Types.NonConstantStruct v = string.Empty; } + { Types.NonConstantStruct v = Constants.String; } + { Types.NonConstantStruct v = constant; } + { Types.NonConstantStruct v = trusted; } + { Types.NonConstantStruct v = variable; } + { Types.NonConstantStruct v = untrusted; } + { + Types.NonConstantStruct v = Constants.String; + v += "abc"; + } + { + Types.NonConstantStruct v = Constants.String; + v += trusted; + } + { + Types.NonConstantStruct v = Constants.String; + v += variable; + } + { + Types.NonConstantStruct v = Constants.String; + v += "a" + "b"; + } + } + + #endregion + + #region Method Reference Tests + + void MethodReferenceTests() { + + { Action action = Types.SomeMethodWithParameter; } + { Action action = /* ReferenceToMethodWithConstantParameterNotSupport */ Types.SomeMethodWithConstantParameter /**/; } + { Action action = /* ReferenceToMethodWithConstantParameterNotSupport */ Types.SomeMethodWithOneConstantParameter /**/; } + { Action action = /* ReferenceToMethodWithConstantParameterNotSupport */ Types.SomeMethodWithOneOtherConstantParameter /**/; } + { Action action = /* ReferenceToMethodWithConstantParameterNotSupport */ Types.SomeMethodWithTwoConstantParameters /**/; } + } + + #endregion + } +}