|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp
{
/// <summary>
/// Traverses the symbol table checking for CLS compliance.
/// </summary>
internal partial class ClsComplianceChecker : CSharpSymbolVisitor
{
private readonly CSharpCompilation _compilation;
private readonly SyntaxTree _filterTree; //if not null, limit analysis to types residing in this tree
private readonly TextSpan? _filterSpanWithinTree; //if filterTree and filterSpanWithinTree is not null, limit analysis to types residing within this span in the filterTree.
private readonly BindingDiagnosticBag _diagnostics;
private readonly CancellationToken _cancellationToken;
private readonly ConcurrentDictionary<Symbol, Compliance> _declaredOrInheritedCompliance;
/// <seealso cref="MethodCompiler._compilerTasks"/>
private readonly ConcurrentStack<Task> _compilerTasks;
private ClsComplianceChecker(
CSharpCompilation compilation,
SyntaxTree filterTree,
TextSpan? filterSpanWithinTree,
BindingDiagnosticBag diagnostics,
CancellationToken cancellationToken)
{
Debug.Assert(diagnostics.DependenciesBag is null || diagnostics.DependenciesBag is ConcurrentSet<AssemblySymbol>);
_compilation = compilation;
_filterTree = filterTree;
_filterSpanWithinTree = filterSpanWithinTree;
_diagnostics = diagnostics;
_cancellationToken = cancellationToken;
_declaredOrInheritedCompliance = new ConcurrentDictionary<Symbol, Compliance>(Symbols.SymbolEqualityComparer.ConsiderEverything);
if (ConcurrentAnalysis)
{
_compilerTasks = new ConcurrentStack<Task>();
}
}
/// <summary>
/// Gets a value indicating whether <see cref="ClsComplianceChecker"/> is allowed to analyze in parallel.
/// </summary>
private bool ConcurrentAnalysis => _filterTree == null && _compilation.Options.ConcurrentBuild;
/// <summary>
/// Traverses the symbol table checking for CLS compliance.
/// </summary>
/// <param name="compilation">Compilation that owns the symbol table.</param>
/// <param name="diagnostics">Will be supplemented with documentation comment diagnostics.</param>
/// <param name="cancellationToken">To stop traversing the symbol table early.</param>
/// <param name="filterTree">Only report diagnostics from this syntax tree, if non-null.</param>
/// <param name="filterSpanWithinTree">If <paramref name="filterTree"/> and <paramref name="filterSpanWithinTree"/> is non-null, report diagnostics within this span in the <paramref name="filterTree"/>.</param>
public static void CheckCompliance(CSharpCompilation compilation, BindingDiagnosticBag diagnostics, CancellationToken cancellationToken, SyntaxTree filterTree = null, TextSpan? filterSpanWithinTree = null)
{
var queue = diagnostics.AccumulatesDependencies ? BindingDiagnosticBag.GetConcurrentInstance() : BindingDiagnosticBag.GetInstance(withDiagnostics: diagnostics.AccumulatesDiagnostics, withDependencies: false);
var checker = new ClsComplianceChecker(compilation, filterTree, filterSpanWithinTree, queue, cancellationToken);
checker.Visit(compilation.Assembly);
checker.WaitForWorkers();
if (diagnostics.AccumulatesDiagnostics)
{
diagnostics.AddRange(queue.DiagnosticBag);
}
diagnostics.AddDependencies(queue);
queue.Free();
}
public override void VisitAssembly(AssemblySymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
System.Diagnostics.Debug.Assert(symbol is SourceAssemblySymbol);
Compliance assemblyCompliance = GetDeclaredOrInheritedCompliance(symbol);
if (assemblyCompliance == Compliance.DeclaredFalse)
{
// Our interpretation of an assembly-level CLSCompliant attribute is as follows:
// 1) If true, then perform all CLS checks.
// 2) If false, then perform no CLS checks (dev11 still performs a few, mostly around
// meaningless attributes). Our interpretation is that the user ultimately wants
// his code to be CLS-compliant, but is temporarily disabling the checks (e.g. during
// refactoring/prototyping).
// 3) If absent, the perform all CLS checks. Our interpretation is that - in the
// absence of an assembly-level attribute - any CLS problems within the compilation
// indicate that the user was trying to do something but didn't know how. It would
// be nice if the most common case (i.e. this one) was the cheapest, but we don't
// want to confuse novice users.
return;
}
bool assemblyComplianceValue = IsTrue(assemblyCompliance);
for (int i = 0; i < symbol.Modules.Length; i++)
{
ModuleSymbol module = symbol.Modules[i];
Location attributeLocation;
bool? moduleDeclaredCompliance = GetDeclaredCompliance(module, out attributeLocation);
Location warningLocation = i == 0 ? attributeLocation : module.GetFirstLocation();
System.Diagnostics.Debug.Assert(warningLocation != null || !moduleDeclaredCompliance.HasValue || (i == 0 && _filterTree != null),
"Can only be null when the source location is filtered out.");
if (moduleDeclaredCompliance.HasValue)
{
if (warningLocation != null)
{
if (!IsDeclared(assemblyCompliance))
{
// This is not useful on non-source modules, but dev11 reports it anyway.
this.AddDiagnostic(ErrorCode.WRN_CLS_NotOnModules, warningLocation);
}
else if (assemblyComplianceValue != moduleDeclaredCompliance.GetValueOrDefault())
{
this.AddDiagnostic(ErrorCode.WRN_CLS_NotOnModules2, warningLocation);
}
}
}
else if (assemblyComplianceValue && i > 0)
{
bool sawClsCompliantAttribute = false;
var peModule = (Symbols.Metadata.PE.PEModuleSymbol)module;
foreach (CSharpAttributeData assemblyLevelAttribute in peModule.GetAssemblyAttributes())
{
if (assemblyLevelAttribute.IsTargetAttribute(AttributeDescription.CLSCompliantAttribute))
{
sawClsCompliantAttribute = true;
break;
}
}
if (!sawClsCompliantAttribute)
{
this.AddDiagnostic(ErrorCode.WRN_CLS_ModuleMissingCLS, warningLocation);
}
}
}
if (assemblyComplianceValue)
{
CheckForAttributeWithArrayArgument(symbol);
}
ModuleSymbol sourceModule = symbol.Modules[0];
if (IsTrue(GetDeclaredOrInheritedCompliance(sourceModule)))
{
CheckForAttributeWithArrayArgument(sourceModule);
}
Visit(symbol.GlobalNamespace);
}
private void WaitForWorkers()
{
var tasks = _compilerTasks;
if (tasks == null)
{
return;
}
while (tasks.TryPop(out Task curTask))
{
curTask.GetAwaiter().GetResult();
}
}
public override void VisitNamespace(NamespaceSymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
if (IsTrue(GetDeclaredOrInheritedCompliance(symbol)))
{
CheckName(symbol);
CheckMemberDistinctness(symbol);
}
if (ConcurrentAnalysis)
{
VisitNamespaceMembersAsTasks(symbol);
}
else
{
VisitNamespaceMembers(symbol);
}
}
private void VisitNamespaceMembersAsTasks(NamespaceSymbol symbol)
{
foreach (var m in symbol.GetMembersUnordered())
{
_compilerTasks.Push(Task.Run(UICultureUtilities.WithCurrentUICulture(() =>
{
try
{
Visit(m);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e))
{
throw ExceptionUtilities.Unreachable();
}
}), _cancellationToken));
}
}
private void VisitNamespaceMembers(NamespaceSymbol symbol)
{
foreach (var m in symbol.GetMembersUnordered())
{
Visit(m);
}
}
[PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", IsParallelEntry = false)]
public override void VisitNamedType(NamedTypeSymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
Debug.Assert(!symbol.IsImplicitClass);
Compliance compliance = GetDeclaredOrInheritedCompliance(symbol);
if (VisitTypeOrMember(symbol, compliance))
{
if (IsTrue(compliance))
{
CheckBaseTypeCompliance(symbol);
CheckTypeParameterCompliance(symbol.TypeParameters, symbol);
if (symbol.TypeKind == TypeKind.Delegate)
{
CheckParameterCompliance(symbol.DelegateInvokeMethod.Parameters, symbol);
}
else if (_compilation.IsAttributeType(symbol) && !HasAcceptableAttributeConstructor(symbol))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_BadAttributeType, symbol.GetFirstLocation(), symbol);
}
}
}
// You may assume we could skip the members if this type is inaccessible,
// but dev11 reports that they are inaccessible as well.
foreach (var m in symbol.GetMembersUnordered())
{
Visit(m);
}
}
private bool HasAcceptableAttributeConstructor(NamedTypeSymbol attributeType)
{
foreach (MethodSymbol constructor in attributeType.InstanceConstructors)
{
if (IsTrue(GetDeclaredOrInheritedCompliance(constructor)) && IsAccessibleIfContainerIsAccessible(constructor))
{
System.Diagnostics.Debug.Assert(IsAccessibleOutsideAssembly(constructor), "Should be implied by IsAccessibleIfContainerIsAccessible");
bool hasUnacceptableParameterType = false;
foreach (var paramType in constructor.ParameterTypesWithAnnotations) // Public caller would select type out of parameters.
{
if (paramType.TypeKind == TypeKind.Array ||
paramType.Type.GetAttributeParameterTypedConstantKind(_compilation) == TypedConstantKind.Error)
{
hasUnacceptableParameterType = true;
break;
}
}
if (!hasUnacceptableParameterType)
{
return true;
}
}
}
return false;
}
public override void VisitMethod(MethodSymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
Compliance compliance = GetDeclaredOrInheritedCompliance(symbol);
// Most CLS checks don't apply to accessors.
if (symbol.IsAccessor())
{
CheckForAttributeOnAccessor(symbol);
CheckForMeaninglessOnParameter(symbol.Parameters);
CheckForMeaninglessOnReturn(symbol);
if (IsTrue(compliance))
{
CheckForAttributeWithArrayArgument(symbol);
}
return;
}
if (!VisitTypeOrMember(symbol, compliance)) return;
if (IsTrue(compliance))
{
CheckParameterCompliance(symbol.Parameters, symbol.ContainingType);
CheckTypeParameterCompliance(symbol.TypeParameters, symbol.ContainingType);
if (symbol.IsVararg)
{
this.AddDiagnostic(ErrorCode.WRN_CLS_NoVarArgs, symbol.GetFirstLocation());
}
}
}
private void CheckForAttributeOnAccessor(MethodSymbol symbol)
{
foreach (CSharpAttributeData attribute in symbol.GetAttributes())
{
if (attribute.IsTargetAttribute(AttributeDescription.CLSCompliantAttribute))
{
Location attributeLocation;
if (TryGetAttributeWarningLocation(attribute, out attributeLocation))
{
AttributeUsageInfo attributeUsage = attribute.AttributeClass.GetAttributeUsageInfo();
this.AddDiagnostic(ErrorCode.ERR_AttributeNotOnAccessor, attributeLocation, attribute.AttributeClass.Name, attributeUsage.GetValidTargetsErrorArgument());
break;
}
}
}
}
public override void VisitProperty(PropertySymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
Compliance compliance = GetDeclaredOrInheritedCompliance(symbol);
if (!VisitTypeOrMember(symbol, compliance)) return;
// Rule 28 requires that the accessors "adhere to a special naming pattern".
// We don't actually need to do anything here, because they automatically
// will unless they override accessors from metadata - we don't check overrides -
// or they explicitly implement interface accessors - we don't check non-public
// members.
if (IsTrue(compliance))
{
CheckParameterCompliance(symbol.Parameters, symbol.ContainingType);
}
}
public override void VisitEvent(EventSymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
Compliance compliance = GetDeclaredOrInheritedCompliance(symbol);
// Rule 32 specifies the shapes of the accessor signatures. However,
// even though WinRT events have a different shape, dev11 does not
// report that they are non-compliant.
// Rule 28 requires that the accessors "adhere to a special naming pattern".
// We don't actually need to do anything here, because they automatically
// will unless they override accessors from metadata - we don't check overrides -
// or they explicitly implement interface accessors - we don't check non-public
// members.
if (!VisitTypeOrMember(symbol, compliance)) return;
}
public override void VisitField(FieldSymbol symbol)
{
_cancellationToken.ThrowIfCancellationRequested();
if (DoNotVisit(symbol)) return;
Compliance compliance = GetDeclaredOrInheritedCompliance(symbol);
if (!VisitTypeOrMember(symbol, compliance)) return;
if (IsTrue(compliance))
{
if (symbol.IsVolatile)
{
this.AddDiagnostic(ErrorCode.WRN_CLS_VolatileField, symbol.GetFirstLocation(), symbol);
}
}
}
/// <returns>False if no further checks are required (because they would be cascading).</returns>
private bool VisitTypeOrMember(Symbol symbol, Compliance compliance)
{
SymbolKind symbolKind = symbol.Kind;
System.Diagnostics.Debug.Assert(
symbolKind == SymbolKind.NamedType ||
symbolKind == SymbolKind.Field ||
symbolKind == SymbolKind.Property ||
symbolKind == SymbolKind.Event ||
symbolKind == SymbolKind.Method);
System.Diagnostics.Debug.Assert(!symbol.IsAccessor());
if (!CheckForDeclarationWithoutAssemblyDeclaration(symbol, compliance))
{
return false; // Don't cascade from this failure.
}
bool isCompliant = IsTrue(compliance);
bool isAccessibleOutsideAssembly = IsAccessibleOutsideAssembly(symbol);
if (isAccessibleOutsideAssembly)
{
if (isCompliant)
{
CheckName(symbol);
CheckForCompliantWithinNonCompliant(symbol);
CheckReturnTypeCompliance(symbol);
if (symbol.Kind == SymbolKind.NamedType)
{
CheckMemberDistinctness((NamedTypeSymbol)symbol);
}
}
else if (GetDeclaredOrInheritedCompliance(symbol.ContainingAssembly) == Compliance.DeclaredTrue && IsTrue(GetInheritedCompliance(symbol)))
{
CheckForNonCompliantAbstractMember(symbol);
}
}
else if (IsDeclared(compliance))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_MeaninglessOnPrivateType, symbol.GetFirstLocation(), symbol);
return false; // Don't cascade from this failure.
}
if (isCompliant)
{
// Independent of accessibility.
CheckForAttributeWithArrayArgument(symbol);
}
// These checks are independent of accessibility and compliance.
if (symbolKind == SymbolKind.NamedType)
{
NamedTypeSymbol type = (NamedTypeSymbol)symbol;
if (type.TypeKind == TypeKind.Delegate)
{
MethodSymbol method = type.DelegateInvokeMethod;
CheckForMeaninglessOnParameter(method.Parameters);
CheckForMeaninglessOnReturn(method);
}
}
else if (symbolKind == SymbolKind.Method)
{
MethodSymbol method = (MethodSymbol)symbol;
CheckForMeaninglessOnParameter(method.Parameters);
CheckForMeaninglessOnReturn(method);
}
else if (symbolKind == SymbolKind.Property)
{
PropertySymbol property = (PropertySymbol)symbol;
CheckForMeaninglessOnParameter(property.Parameters);
}
// All checks that apply to inaccessible symbols are performed by this method.
return isAccessibleOutsideAssembly;
}
private void CheckForNonCompliantAbstractMember(Symbol symbol)
{
System.Diagnostics.Debug.Assert(!IsTrue(GetDeclaredOrInheritedCompliance(symbol)), "Only call on non-compliant symbols");
NamedTypeSymbol containingType = symbol.ContainingType;
if ((object)containingType != null && containingType.IsInterface)
{
this.AddDiagnostic(ErrorCode.WRN_CLS_BadInterfaceMember, symbol.GetFirstLocation(), symbol);
}
else if (symbol.IsAbstract && symbol.Kind != SymbolKind.NamedType)
{
this.AddDiagnostic(ErrorCode.WRN_CLS_NoAbstractMembers, symbol.GetFirstLocation(), symbol);
}
}
private void CheckBaseTypeCompliance(NamedTypeSymbol symbol)
{
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)), "Only call on compliant symbols");
// NOTE: implemented interfaces do not have to be CLS-compliant (unless the type itself is an interface).
if (symbol.IsInterface)
{
foreach (NamedTypeSymbol interfaceType in symbol.InterfacesNoUseSiteDiagnostics())
{
if (!IsCompliantType(interfaceType, symbol))
{
// TODO: it would be nice to report this on the base type clause.
this.AddDiagnostic(ErrorCode.WRN_CLS_BadInterface, symbol.GetFirstLocation(), symbol, interfaceType);
}
}
}
else
{
NamedTypeSymbol baseType = symbol.EnumUnderlyingType ?? symbol.BaseTypeNoUseSiteDiagnostics; // null for interfaces
System.Diagnostics.Debug.Assert((object)baseType != null || symbol.SpecialType == SpecialType.System_Object, "Only object has no base.");
if ((object)baseType != null && !IsCompliantType(baseType, symbol))
{
// TODO: it would be nice to report this on the base type clause.
this.AddDiagnostic(ErrorCode.WRN_CLS_BadBase, symbol.GetFirstLocation(), symbol, baseType);
}
}
}
private void CheckForCompliantWithinNonCompliant(Symbol symbol)
{
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)), "Only call on compliant symbols");
NamedTypeSymbol containingType = symbol.ContainingType;
System.Diagnostics.Debug.Assert((object)containingType == null || !containingType.IsImplicitClass);
if ((object)containingType != null && !IsTrue(GetDeclaredOrInheritedCompliance(containingType)))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_IllegalTrueInFalse, symbol.GetFirstLocation(), symbol, containingType);
}
}
private void CheckTypeParameterCompliance(ImmutableArray<TypeParameterSymbol> typeParameters, NamedTypeSymbol context)
{
System.Diagnostics.Debug.Assert(typeParameters.IsEmpty || IsTrue(GetDeclaredOrInheritedCompliance(context)), "Only call on compliant symbols");
foreach (TypeParameterSymbol typeParameter in typeParameters)
{
foreach (TypeWithAnnotations constraintType in typeParameter.ConstraintTypesNoUseSiteDiagnostics)
{
if (!IsCompliantType(constraintType.Type, context))
{
// TODO: it would be nice to report this on the constraint clause.
// NOTE: we're improving over dev11 by reporting on the type parameter declaration,
// rather than on the constraint type declaration.
this.AddDiagnostic(ErrorCode.WRN_CLS_BadTypeVar, typeParameter.GetFirstLocation(), constraintType.Type);
}
}
}
}
private void CheckParameterCompliance(ImmutableArray<ParameterSymbol> parameters, NamedTypeSymbol context)
{
System.Diagnostics.Debug.Assert(parameters.IsEmpty || IsTrue(GetDeclaredOrInheritedCompliance(context)), "Only call on compliant symbols");
foreach (ParameterSymbol parameter in parameters)
{
if (!IsCompliantType(parameter.Type, context))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_BadArgType, parameter.GetFirstLocation(), parameter.Type);
}
}
}
private void CheckForAttributeWithArrayArgument(Symbol symbol)
{
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)), "Only call on compliant symbols");
CheckForAttributeWithArrayArgumentInternal(symbol.GetAttributes());
if (symbol.Kind == SymbolKind.Method)
{
CheckForAttributeWithArrayArgumentInternal(((MethodSymbol)symbol).GetReturnTypeAttributes());
}
}
/// <remarks>
/// BREAK: Dev11 reports WRN_CLS_ArrayArgumentToAttribute on all symbols, whereas roslyn reports it only
/// on accessible symbols.
/// </remarks>
private void CheckForAttributeWithArrayArgumentInternal(ImmutableArray<CSharpAttributeData> attributes)
{
foreach (CSharpAttributeData attribute in attributes)
{
foreach (TypedConstant argument in attribute.ConstructorArguments)
{
if (argument.TypeInternal.TypeKind == TypeKind.Array)
{
// TODO: it would be nice to report for each bad argument, but currently it's pointless since they
// would all have the same message and location.
Location warningLocation;
if (TryGetAttributeWarningLocation(attribute, out warningLocation))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_ArrayArgumentToAttribute, warningLocation);
return;
}
}
}
foreach (var pair in attribute.NamedArguments)
{
TypedConstant argument = pair.Value;
if (argument.TypeInternal.TypeKind == TypeKind.Array)
{
// TODO: it would be nice to report for each bad argument, but currently it's pointless since they
// would all have the same message and location.
Location warningLocation;
if (TryGetAttributeWarningLocation(attribute, out warningLocation))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_ArrayArgumentToAttribute, warningLocation);
return;
}
}
}
// This catches things like param arrays and converted null literals.
if ((object)attribute.AttributeConstructor != null) // Happens in error scenarios.
{
foreach (var type in attribute.AttributeConstructor.ParameterTypesWithAnnotations)
{
if (type.TypeKind == TypeKind.Array)
{
// TODO: it would be nice to report for each bad argument, but currently it's pointless since they
// would all have the same message and location.
Location warningLocation;
if (TryGetAttributeWarningLocation(attribute, out warningLocation))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_ArrayArgumentToAttribute, warningLocation);
return;
}
}
}
}
}
}
private bool TryGetAttributeWarningLocation(CSharpAttributeData attribute, out Location location)
{
SyntaxReference syntaxRef = attribute.ApplicationSyntaxReference;
if (syntaxRef == null && _filterTree == null)
{
location = NoLocation.Singleton;
return true;
}
else if (_filterTree == null || (syntaxRef != null && syntaxRef.SyntaxTree == _filterTree))
{
System.Diagnostics.Debug.Assert(syntaxRef.SyntaxTree.HasCompilationUnitRoot);
location = new SourceLocation(syntaxRef);
return true;
}
location = null;
return false;
}
private void CheckForMeaninglessOnParameter(ImmutableArray<ParameterSymbol> parameters)
{
if (parameters.IsEmpty) return;
int startPos = 0;
Symbol container = parameters[0].ContainingSymbol;
if (container.Kind == SymbolKind.Method)
{
Symbol associated = ((MethodSymbol)container).AssociatedSymbol;
if ((object)associated != null && associated.Kind == SymbolKind.Property)
{
// Only care about "value" parameter for accessors.
// NOTE: public caller would have to count parameters.
startPos = ((PropertySymbol)associated).ParameterCount;
}
}
for (int i = startPos; i < parameters.Length; i++)
{
Location attributeLocation;
if (TryGetClsComplianceAttributeLocation(parameters[i].GetAttributes(), out attributeLocation))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_MeaninglessOnParam, attributeLocation);
}
}
}
private void CheckForMeaninglessOnReturn(MethodSymbol method)
{
Location attributeLocation;
if (TryGetClsComplianceAttributeLocation(method.GetReturnTypeAttributes(), out attributeLocation))
{
this.AddDiagnostic(ErrorCode.WRN_CLS_MeaninglessOnReturn, attributeLocation);
}
}
private void CheckReturnTypeCompliance(Symbol symbol)
{
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)), "Only call on compliant symbols");
ErrorCode code;
TypeSymbol type;
switch (symbol.Kind)
{
case SymbolKind.Field:
code = ErrorCode.WRN_CLS_BadFieldPropType;
type = ((FieldSymbol)symbol).Type;
break;
case SymbolKind.Property:
code = ErrorCode.WRN_CLS_BadFieldPropType;
type = ((PropertySymbol)symbol).Type;
break;
case SymbolKind.Event:
code = ErrorCode.WRN_CLS_BadFieldPropType;
type = ((EventSymbol)symbol).Type;
break;
case SymbolKind.Method:
code = ErrorCode.WRN_CLS_BadReturnType;
MethodSymbol method = (MethodSymbol)symbol;
type = method.ReturnType;
if (method.MethodKind == MethodKind.DelegateInvoke)
{
System.Diagnostics.Debug.Assert(method.ContainingType.TypeKind == TypeKind.Delegate);
symbol = method.ContainingType; // Refer to the delegate type in diagnostics.
}
// Diagnostic not interesting for accessors.
System.Diagnostics.Debug.Assert(!method.IsAccessor());
break;
case SymbolKind.NamedType:
symbol = ((NamedTypeSymbol)symbol).DelegateInvokeMethod;
if ((object)symbol == null)
{
return;
}
else
{
goto case SymbolKind.Method;
}
default:
throw ExceptionUtilities.UnexpectedValue(symbol.Kind);
}
if (!IsCompliantType(type, symbol.ContainingType))
{
this.AddDiagnostic(code, symbol.GetFirstLocation(), symbol);
}
}
private bool TryGetClsComplianceAttributeLocation(ImmutableArray<CSharpAttributeData> attributes, out Location attributeLocation)
{
foreach (CSharpAttributeData data in attributes)
{
if (data.IsTargetAttribute(AttributeDescription.CLSCompliantAttribute))
{
if (TryGetAttributeWarningLocation(data, out attributeLocation))
{
return true;
}
}
}
attributeLocation = null;
return false;
}
/// <returns>True if the symbol is okay (i.e. no warnings).</returns>
private bool CheckForDeclarationWithoutAssemblyDeclaration(Symbol symbol, Compliance compliance)
{
if (IsDeclared(compliance))
{
Compliance assemblyCompliance = GetDeclaredOrInheritedCompliance(symbol.ContainingAssembly);
if (!IsDeclared(assemblyCompliance))
{
ErrorCode code = IsTrue(compliance)
? ErrorCode.WRN_CLS_AssemblyNotCLS
: ErrorCode.WRN_CLS_AssemblyNotCLS2;
this.AddDiagnostic(code, symbol.GetFirstLocation(), symbol);
return false;
}
}
return true;
}
private void CheckMemberDistinctness(NamespaceOrTypeSymbol symbol)
{
System.Diagnostics.Debug.Assert(IsAccessibleOutsideAssembly(symbol));
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)));
MultiDictionary<string, Symbol> seenByName = new MultiDictionary<string, Symbol>(CaseInsensitiveComparison.Comparer);
// For types, we also need to consider collisions with inherited members.
if (symbol.Kind != SymbolKind.Namespace)
{
NamedTypeSymbol type = (NamedTypeSymbol)symbol;
// NOTE: As in dev11 we're using Interfaces, rather than AllInterfaces.
// This seems like a bug, but it's easier to reproduce it than to deal
// with all the potential breaks.
// NOTE: It's not clear why dev11 is looking in interfaces at all. Maybe
// it was only supposed to happen for interface types?
foreach (NamedTypeSymbol @interface in type.InterfacesAndTheirBaseInterfacesNoUseSiteDiagnostics.Keys) // NOTE: would be hand-rolled in a standalone component.
{
if (!IsAccessibleOutsideAssembly(@interface)) continue;
foreach (Symbol member in @interface.GetMembersUnordered())
{
// NOTE: As in dev11 we filter out overriding methods and properties (but not events).
// NOTE: As in dev11, we ignore the CLS compliance of the interface and its members.
if (IsAccessibleIfContainerIsAccessible(member) &&
(!member.IsOverride || !(member.Kind == SymbolKind.Method || member.Kind == SymbolKind.Property)))
{
seenByName.Add(member.Name, member);
}
}
}
NamedTypeSymbol baseType = type.BaseTypeNoUseSiteDiagnostics;
while ((object)baseType != null)
{
foreach (Symbol member in baseType.GetMembersUnordered())
{
// NOTE: As in dev11 we filter out overriding methods and properties (but not events).
// NOTE: Unlike for interface members, we check CLS compliance of base type members.
if (IsAccessibleOutsideAssembly(member) &&
IsTrue(GetDeclaredOrInheritedCompliance(member)) &&
(!member.IsOverride || !(member.Kind == SymbolKind.Method || member.Kind == SymbolKind.Property)))
{
seenByName.Add(member.Name, member);
}
}
baseType = baseType.BaseTypeNoUseSiteDiagnostics;
}
}
// NOTE: visit the members in order so that the same one is always reported as a conflict.
foreach (Symbol member in symbol.GetMembers())
{
// Filter out uninteresting members:
if (DoNotVisit(member) ||
!IsAccessibleIfContainerIsAccessible(member) || // We already know that the container is accessible.
!IsTrue(GetDeclaredOrInheritedCompliance(member)) ||
member.IsOverride)
{
continue;
}
var name = member.Name;
var sameNameSymbols = seenByName[name];
if (sameNameSymbols.Count > 0)
{
CheckSymbolDistinctness(member, name, sameNameSymbols);
}
seenByName.Add(name, member);
}
}
/// <remarks>
/// NOTE: Dev11 behavior - First, it ignores arity,
/// which seems like a good way to disambiguate symbols (in particular,
/// CLS Rule 43 says that the name includes backtick-arity). Second, it
/// does not consider two members with identical names (i.e. not differing
/// in case) to collide.
/// </remarks>
private void CheckSymbolDistinctness(Symbol symbol, string symbolName, MultiDictionary<string, Symbol>.ValueSet sameNameSymbols)
{
Debug.Assert(sameNameSymbols.Count > 0);
Debug.Assert(symbol.Name == symbolName);
bool isMethodOrProperty = symbol.Kind == SymbolKind.Method || symbol.Kind == SymbolKind.Property;
foreach (Symbol other in sameNameSymbols)
{
if (other.Name != symbolName && !(isMethodOrProperty && other.Kind == symbol.Kind))
{
// TODO: Shouldn't we somehow reference the conflicting member? Dev11 doesn't.
this.AddDiagnostic(ErrorCode.WRN_CLS_BadIdentifierCase, symbol.GetFirstLocation(), symbol);
return;
}
}
if (!isMethodOrProperty)
{
return;
}
foreach (Symbol other in sameNameSymbols)
{
// Note: not checking accessor signatures, but checking accessor names.
ErrorCode code;
if (symbol.Kind == other.Kind &&
!symbol.IsAccessor() &&
!other.IsAccessor() &&
TryGetCollisionErrorCode(symbol, other, out code))
{
this.AddDiagnostic(code, symbol.GetFirstLocation(), symbol);
return;
}
else if (other.Name != symbolName)
{
// TODO: Shouldn't we somehow reference the conflicting member? Dev11 doesn't.
this.AddDiagnostic(ErrorCode.WRN_CLS_BadIdentifierCase, symbol.GetFirstLocation(), symbol);
return;
}
}
}
private void CheckName(Symbol symbol)
{
System.Diagnostics.Debug.Assert(IsTrue(GetDeclaredOrInheritedCompliance(symbol)));
System.Diagnostics.Debug.Assert(IsAccessibleOutsideAssembly(symbol));
if (!symbol.CanBeReferencedByName || symbol.IsOverride) return;
string name = symbol.Name;
// NOTE: The CLI spec says
// CLS Rule 4: Assemblies shall follow Annex 7 of Technical Report 15 of the Unicode Standard 3.0 governing
// the set of characters permitted to start and be included in identifiers, available on-line at
// http://www.unicode.org/unicode/reports/tr15/tr15-18.html. Identifiers shall be in the canonical format defined
// by Unicode Normalization Form C. For CLS purposes, two identifiers are the same if their lowercase mappings
// (as specified by the Unicode locale-insensitive, one-to-one lowercase mappings) are the same. That is, for two
// identifiers to be considered different under the CLS they shall differ in more than simply their case. However,
// in order to override an inherited definition the CLI requires the precise encoding of the original declaration be
// used.
//
// However, what the native compiler actually does is ignore everything about composed and decomposed characters
// (see comment in CompilationPass::checkCLSnaming) and then just checks if the first character is underscore
// (0x005F or 0xFF3F). Presumably, it assumes that the language rules have weeded out any other identifiers
// forbidden by the unicode spec.
// NOTE: The parser won't actually accept '\uFF3F' as part of an identifier.
System.Diagnostics.Debug.Assert(name.Length == 0 || name[0] != '\uFF3F');
if (name.Length > 0 && name[0] == '\u005F')
{
this.AddDiagnostic(ErrorCode.WRN_CLS_BadIdentifier, symbol.GetFirstLocation(), name);
}
}
private bool DoNotVisit(Symbol symbol)
{
if (symbol.Kind == SymbolKind.Namespace)
{
return false;
}
// TODO: There's no public equivalent of Symbol.DeclaringCompilation.
return symbol.DeclaringCompilation != _compilation ||
symbol.IsImplicitlyDeclared ||
IsSyntacticallyFilteredOut(symbol);
}
private bool IsSyntacticallyFilteredOut(Symbol symbol)
{
// TODO: it would be nice to be more precise than this: we only want to
// warn about the base class if it is listed in the filter tree, not if
// any part of the type is in the filter tree.
return _filterTree != null && !symbol.IsDefinedInSourceTree(_filterTree, _filterSpanWithinTree);
}
private bool IsCompliantType(TypeSymbol type, NamedTypeSymbol context)
{
switch (type.TypeKind)
{
case TypeKind.Array:
return IsCompliantType(((ArrayTypeSymbol)type).ElementType, context);
case TypeKind.Dynamic:
// NOTE: It would probably be most correct to return
// IsCompliantType(this.compilation.GetSpecialType(SpecialType.System_Object), context)
// but that's way too much work in the 99.9% case.
return true;
case TypeKind.Pointer:
case TypeKind.FunctionPointer:
return false;
case TypeKind.Error:
case TypeKind.TypeParameter:
// Possibly not the most accurate answer, but the gist is that we
// don't want to report problems with these types.
return true;
case TypeKind.Class:
case TypeKind.Struct:
case TypeKind.Interface:
case TypeKind.Delegate:
case TypeKind.Enum:
case TypeKind.Submission:
return IsCompliantType((NamedTypeSymbol)type, context);
default:
throw ExceptionUtilities.UnexpectedValue(type.TypeKind);
}
}
private bool IsCompliantType(NamedTypeSymbol type, NamedTypeSymbol context)
{
// BREAK: Other than in the cases listed below, dev11 always
// returns true for predefined types - even if they are constructed
// with non-compliant type arguments (e.g. System.Action<uint>).
switch (type.SpecialType)
{
case SpecialType.System_TypedReference:
case SpecialType.System_UIntPtr:
// Hard-coded in dev11 (LangCompiler::isCLS_Type).
return false;
case SpecialType.System_SByte: // sic
case SpecialType.System_UInt16:
case SpecialType.System_UInt32:
case SpecialType.System_UInt64:
// Dev11 calls these "quasi-simple" types and hard-codes false.
return false;
}
if (type.TypeKind == TypeKind.Error)
{
// Possibly not the most accurate answer, but the gist is that we
// don't want to report problems with these types.
return true;
}
if (!IsTrue(GetDeclaredOrInheritedCompliance(type.OriginalDefinition)))
{
return false;
}
foreach (TypeWithAnnotations typeArg in type.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics)
{
if (!IsCompliantType(typeArg.Type, context))
{
return false;
}
}
return !IsInaccessibleBecauseOfConstruction(type, context);
}
/// <remarks>
/// This check (the only one that uses the "context" parameter is based on CLS Rule 46,
/// as implemented by LangCompiler::IsCLSAccessible. The idea is that C<int> and C<char>
/// are separate types in CLS, so they can't touch each other's protected members.
/// TODO: This should really have a separate error code - it's logically separate and requires explanation.
/// </remarks>
/// <param name="type">Check the accessibility of this type (probably a parameter or return type).</param>
/// <param name="context">Context for the accessibility check (e.g. containing type of method with <paramref name="type"/> as a parameter type.</param>
private static bool IsInaccessibleBecauseOfConstruction(NamedTypeSymbol type, NamedTypeSymbol context)
{
// NOTE: Dev11 (incorrectly) only checks whether "type" is protected - it ignores container accessibility.
bool sawProtected = type.DeclaredAccessibility.HasProtected();
bool sawGeneric = false; // Generic "type" doesn't count.
Dictionary<NamedTypeSymbol, NamedTypeSymbol> containingTypes = null; // maps definition to constructed
{
NamedTypeSymbol containingType = type.ContainingType;
while ((object)containingType != null)
{
if (containingTypes == null)
{
containingTypes = new Dictionary<NamedTypeSymbol, NamedTypeSymbol>();
}
sawProtected = sawProtected || containingType.DeclaredAccessibility.HasProtected();
sawGeneric = sawGeneric || containingType.Arity > 0;
containingTypes.Add(containingType.OriginalDefinition, containingType);
containingType = containingType.ContainingType;
}
}
if (!sawProtected || !sawGeneric || containingTypes == null)
{
return false;
}
while ((object)context != null)
{
NamedTypeSymbol contextBaseType = context;
while ((object)contextBaseType != null)
{
NamedTypeSymbol containingType;
if (containingTypes.TryGetValue(contextBaseType.OriginalDefinition, out containingType))
{
return !TypeSymbol.Equals(containingType, contextBaseType, TypeCompareKind.AllIgnoreOptions);
}
contextBaseType = contextBaseType.BaseTypeNoUseSiteDiagnostics;
}
context = context.ContainingType;
}
// NOTE: Dev11 seems to return true here. That's reasonable, since the type is inaccessible,
// but it seems like the inaccessibility can only be cascading from another error.
return false;
}
private Compliance GetDeclaredOrInheritedCompliance(Symbol symbol)
{
System.Diagnostics.Debug.Assert(symbol.Kind == SymbolKind.NamedType || !((symbol is TypeSymbol)),
"Type kinds without declarations are handled elsewhere.");
if (symbol.Kind == SymbolKind.Namespace)
{
// Don't bother storing entries for namespaces - just go straight to the assembly.
return GetDeclaredOrInheritedCompliance(symbol.ContainingAssembly);
}
else if (symbol.Kind == SymbolKind.Method)
{
MethodSymbol method = (MethodSymbol)symbol;
Symbol associated = method.AssociatedSymbol;
if ((object)associated != null)
{
// Don't bother storing entries for accessors - just go straight to the property/event.
return GetDeclaredOrInheritedCompliance(associated);
}
}
// Not meaningful
Debug.Assert(symbol.Kind != SymbolKind.Alias);
Debug.Assert(symbol.Kind != SymbolKind.Label);
Debug.Assert(symbol.Kind != SymbolKind.Namespace);
Debug.Assert(symbol.Kind != SymbolKind.Parameter);
Debug.Assert(symbol.Kind != SymbolKind.RangeVariable);
Compliance compliance;
if (_declaredOrInheritedCompliance.TryGetValue(symbol, out compliance))
{
return compliance;
}
Location ignoredLocation;
bool? declaredCompliance = GetDeclaredCompliance(symbol, out ignoredLocation);
if (declaredCompliance.HasValue)
{
compliance = declaredCompliance.GetValueOrDefault() ? Compliance.DeclaredTrue : Compliance.DeclaredFalse;
}
else if (symbol.Kind == SymbolKind.Assembly)
{
// Assemblies are not compliant unless specifically declared to be so.
compliance = Compliance.ImpliedFalse;
}
else
{
compliance = IsTrue(GetInheritedCompliance(symbol)) ? Compliance.InheritedTrue : Compliance.InheritedFalse;
}
// Don't bother caching methods, etc - they won't be reused.
return (symbol.Kind == SymbolKind.Assembly || symbol.Kind == SymbolKind.NamedType)
? _declaredOrInheritedCompliance.GetOrAdd(symbol, compliance)
: compliance;
}
private Compliance GetInheritedCompliance(Symbol symbol)
{
System.Diagnostics.Debug.Assert(symbol.Kind != SymbolKind.Assembly);
Symbol containing = (Symbol)symbol.ContainingType ?? symbol.ContainingAssembly;
System.Diagnostics.Debug.Assert((object)containing != null);
return GetDeclaredOrInheritedCompliance(containing);
}
/// <remarks>
/// As in dev11, we ignore the fact that CLSCompliantAttribute is inherited (i.e. from the base type)
/// (see CSemanticChecker::CheckSymForCLS). This should only affect types where the syntactic parent
/// and the inheritance parent disagree.
/// </remarks>
private bool? GetDeclaredCompliance(Symbol symbol, out Location attributeLocation)
{
attributeLocation = null;
foreach (CSharpAttributeData data in symbol.GetAttributes())
{
// Check signature before HasErrors to avoid realizing symbols for other attributes.
if (data.IsTargetAttribute(AttributeDescription.CLSCompliantAttribute))
{
NamedTypeSymbol attributeClass = data.AttributeClass;
if ((object)attributeClass != null)
{
if (_diagnostics.ReportUseSite(attributeClass, symbol.GetFirstLocationOrNone()))
{
continue;
}
}
if (!data.HasErrors)
{
if (!TryGetAttributeWarningLocation(data, out attributeLocation))
{
attributeLocation = null;
}
ImmutableArray<TypedConstant> args = data.CommonConstructorArguments;
System.Diagnostics.Debug.Assert(args.Length == 1, "We already checked the signature and HasErrors.");
// Duplicates are reported elsewhere - we only care about the first (error-free) occurrence.
return (bool)args[0].ValueInternal;
}
}
}
return null;
}
private static bool IsAccessibleOutsideAssembly(Symbol symbol)
{
while ((object)symbol != null && !IsImplicitClass(symbol))
{
if (!IsAccessibleIfContainerIsAccessible(symbol))
{
return false;
}
symbol = symbol.ContainingType;
}
return true;
}
private static bool IsAccessibleIfContainerIsAccessible(Symbol symbol)
{
switch (symbol.DeclaredAccessibility)
{
case Accessibility.Public:
case Accessibility.Protected:
case Accessibility.ProtectedOrInternal:
return true;
case Accessibility.Private:
case Accessibility.ProtectedAndInternal:
case Accessibility.Internal:
return false;
case Accessibility.NotApplicable:
System.Diagnostics.Debug.Assert(symbol.Kind == SymbolKind.ErrorType);
return false;
default:
throw ExceptionUtilities.UnexpectedValue(symbol.DeclaredAccessibility);
}
}
private void AddDiagnostic(ErrorCode code, Location location)
{
var info = new CSDiagnosticInfo(code);
var diag = new CSDiagnostic(info, location);
_diagnostics.Add(diag);
}
private void AddDiagnostic(ErrorCode code, Location location, params object[] args)
{
var info = new CSDiagnosticInfo(code, args);
var diag = new CSDiagnostic(info, location);
_diagnostics.Add(diag);
}
private static bool IsImplicitClass(Symbol symbol)
{
return symbol.Kind == SymbolKind.NamedType && ((NamedTypeSymbol)symbol).IsImplicitClass;
}
private static bool IsTrue(Compliance compliance)
{
switch (compliance)
{
case Compliance.DeclaredTrue:
case Compliance.InheritedTrue:
return true;
case Compliance.DeclaredFalse:
case Compliance.InheritedFalse:
case Compliance.ImpliedFalse:
return false;
default:
throw ExceptionUtilities.UnexpectedValue(compliance);
}
}
private static bool IsDeclared(Compliance compliance)
{
switch (compliance)
{
case Compliance.DeclaredTrue:
case Compliance.DeclaredFalse:
return true;
case Compliance.InheritedTrue:
case Compliance.InheritedFalse:
case Compliance.ImpliedFalse:
return false;
default:
throw ExceptionUtilities.UnexpectedValue(compliance);
}
}
private enum Compliance
{
DeclaredTrue,
DeclaredFalse,
InheritedTrue,
InheritedFalse,
ImpliedFalse,
}
/// <remarks>
/// Based on CompilationPass::CLSReduceSignature.
/// </remarks>
private static bool TryGetCollisionErrorCode(Symbol x, Symbol y, out ErrorCode code)
{
System.Diagnostics.Debug.Assert((object)x != null);
System.Diagnostics.Debug.Assert((object)y != null);
System.Diagnostics.Debug.Assert((object)x != (object)y);
System.Diagnostics.Debug.Assert(x.Kind == y.Kind);
code = ErrorCode.Void;
ImmutableArray<TypeWithAnnotations> xParameterTypes;
ImmutableArray<TypeWithAnnotations> yParameterTypes;
ImmutableArray<RefKind> xRefKinds;
ImmutableArray<RefKind> yRefKinds;
switch (x.Kind)
{
case SymbolKind.Method:
var mX = (MethodSymbol)x;
xParameterTypes = mX.ParameterTypesWithAnnotations;
xRefKinds = mX.ParameterRefKinds;
var mY = (MethodSymbol)y;
yParameterTypes = mY.ParameterTypesWithAnnotations;
yRefKinds = mY.ParameterRefKinds;
break;
case SymbolKind.Property:
var pX = (PropertySymbol)x;
xParameterTypes = pX.ParameterTypesWithAnnotations;
xRefKinds = pX.ParameterRefKinds;
var pY = (PropertySymbol)y;
yParameterTypes = pY.ParameterTypesWithAnnotations;
yRefKinds = pY.ParameterRefKinds;
break;
default:
throw ExceptionUtilities.UnexpectedValue(x.Kind);
}
int numParams = xParameterTypes.Length;
if (yParameterTypes.Length != numParams)
{
return false;
}
// Compare parameters without regard for RefKind (or other modifier),
// array rank, or unnamed array element types (e.g. int[][] == char[][]).
bool sawRefKindDifference = xRefKinds.IsDefault != yRefKinds.IsDefault;
bool sawArrayRankDifference = false;
bool sawArrayOfArraysDifference = false;
for (int i = 0; i < numParams; i++)
{
TypeSymbol xType = xParameterTypes[i].Type;
TypeSymbol yType = yParameterTypes[i].Type;
TypeKind typeKind = xType.TypeKind;
if (yType.TypeKind != typeKind)
{
return false;
}
if (typeKind == TypeKind.Array)
{
ArrayTypeSymbol xArrayType = (ArrayTypeSymbol)xType;
ArrayTypeSymbol yArrayType = (ArrayTypeSymbol)yType;
sawArrayRankDifference = sawArrayRankDifference || xArrayType.Rank != yArrayType.Rank;
bool elementTypesDiffer = !TypeSymbol.Equals(xArrayType.ElementType, yArrayType.ElementType, TypeCompareKind.ConsiderEverything2);
// You might expect that only unnamed-vs-unnamed would produce a warning, but
// dev11 reports unnamed-vs-anything.
if (IsArrayOfArrays(xArrayType) || IsArrayOfArrays(yArrayType))
{
sawArrayOfArraysDifference = sawArrayOfArraysDifference || elementTypesDiffer;
}
else if (elementTypesDiffer)
{
return false;
}
}
else if (!TypeSymbol.Equals(xType, yType, TypeCompareKind.ConsiderEverything2))
{
return false;
}
if (!xRefKinds.IsDefault)
{
sawRefKindDifference = sawRefKindDifference || xRefKinds[i] != yRefKinds[i];
}
}
code =
sawArrayOfArraysDifference ? ErrorCode.WRN_CLS_OverloadUnnamed :
sawArrayRankDifference ? ErrorCode.WRN_CLS_OverloadRefOut : // Lumping rank difference with refkind is odd, but matches dev11.
sawRefKindDifference ? ErrorCode.WRN_CLS_OverloadRefOut :
ErrorCode.Void;
return code != ErrorCode.Void;
}
private static bool IsArrayOfArrays(ArrayTypeSymbol arrayType)
{
return arrayType.ElementType.Kind == SymbolKind.ArrayType;
}
}
}
|