diff --git a/Directory.Build.props b/Directory.Build.props index e0cd93ede..17bbe6dff 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,9 +13,9 @@ all runtime; build; native; contentfiles; analyzers - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + false + Analyzer + diff --git a/pythonnet.sln b/pythonnet.sln index 4ca4fb285..3151454c3 100644 --- a/pythonnet.sln +++ b/pythonnet.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30717.126 MinimumVisualStudioVersion = 15.0.26124.0 @@ -46,6 +46,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{142A6752 Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D4963EF0-46CD-43AF-939D-BA47C1B091D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NonCopyable", "src\noncopyable_analyzer\NonCopyable.csproj", "{CA041F36-A4C2-4B18-9501-F670FDED87F4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -152,6 +156,18 @@ Global {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x64.Build.0 = Release|Any CPU {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x86.ActiveCfg = Release|Any CPU {35CBBDEB-FC07-4D04-9D3E-F88FC180110B}.Release|x86.Build.0 = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|x64.Build.0 = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Debug|x86.Build.0 = Debug|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|Any CPU.Build.0 = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|x64.ActiveCfg = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|x64.Build.0 = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|x86.ActiveCfg = Release|Any CPU + {CA041F36-A4C2-4B18-9501-F670FDED87F4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -159,4 +175,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C8845072-C642-4858-8627-27E862AD21BB} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {CA041F36-A4C2-4B18-9501-F670FDED87F4} = {D4963EF0-46CD-43AF-939D-BA47C1B091D1} + EndGlobalSection EndGlobal diff --git a/src/noncopyable_analyzer/LICENSE b/src/noncopyable_analyzer/LICENSE new file mode 100644 index 000000000..76b0b9f58 --- /dev/null +++ b/src/noncopyable_analyzer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Nobuyuki Iwanaga + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/noncopyable_analyzer/NonCopyable.csproj b/src/noncopyable_analyzer/NonCopyable.csproj new file mode 100644 index 000000000..ef8890d2b --- /dev/null +++ b/src/noncopyable_analyzer/NonCopyable.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + false + True + latest + + + + PythonNet.NonCopyableAnalyzer + Nobuyuki Iwanaga + https://github.com/ufcpp/NonCopyableAnalyzer/blob/master/LICENSE + https://github.com/ufcpp/NonCopyableAnalyzer + false + Analyzer for Non-copyable struct + Fixed false positive on conversion operators with in argument. + NonCopyable, analyzers + true + + + + + + + + diff --git a/src/noncopyable_analyzer/NonCopyableAnalyzer.cs b/src/noncopyable_analyzer/NonCopyableAnalyzer.cs new file mode 100644 index 000000000..ffbbbacb4 --- /dev/null +++ b/src/noncopyable_analyzer/NonCopyableAnalyzer.cs @@ -0,0 +1,256 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace NonCopyable +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class NonCopyableAnalyzer : DiagnosticAnalyzer + { + private static DiagnosticDescriptor CreateRule(int num, string type) + => new DiagnosticDescriptor("NoCopy" + num.ToString("00"), "non-copyable", "🚫 " + type + ". '{0}' is non-copyable.", "Correction", DiagnosticSeverity.Error, isEnabledByDefault: true); + + private static DiagnosticDescriptor FieldDeclarationRule = CreateRule(1, "field declaration"); + private static DiagnosticDescriptor InitializerRule = CreateRule(2, "initializer"); + private static DiagnosticDescriptor AssignmentRule = CreateRule(3, "assignment"); + private static DiagnosticDescriptor ArgumentRule = CreateRule(4, "argument"); + private static DiagnosticDescriptor ReturnRule = CreateRule(5, "return"); + private static DiagnosticDescriptor ConversionRule = CreateRule(6, "conversion"); + private static DiagnosticDescriptor PatternRule = CreateRule(7, "pattern matching"); + private static DiagnosticDescriptor TupleRule = CreateRule(8, "tuple"); + private static DiagnosticDescriptor MemberRule = CreateRule(9, "member reference"); + private static DiagnosticDescriptor ReadOnlyInvokeRule = CreateRule(10, "readonly invoke"); + private static DiagnosticDescriptor GenericConstraintRule = CreateRule(11, "generic constraint"); + private static DiagnosticDescriptor DelegateRule = CreateRule(12, "delegate"); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(FieldDeclarationRule, InitializerRule, AssignmentRule, ArgumentRule, ReturnRule, ConversionRule, PatternRule, TupleRule, MemberRule, ReadOnlyInvokeRule, GenericConstraintRule, DelegateRule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterCompilationStartAction(csc => + { + csc.RegisterOperationAction(oc => + { + var op = (ISymbolInitializerOperation)oc.Operation; + CheckCopyability(oc, op.Value, InitializerRule); + }, OperationKind.FieldInitializer, + OperationKind.ParameterInitializer, + OperationKind.PropertyInitializer, + OperationKind.VariableInitializer); + + csc.RegisterOperationAction(oc => + { + // including member initializer + // including collection element initializer + var op = (ISimpleAssignmentOperation)oc.Operation; + if (op.IsRef) return; + CheckCopyability(oc, op.Value, AssignmentRule); + }, OperationKind.SimpleAssignment); + + csc.RegisterOperationAction(oc => + { + // including non-ref extension method invocation + var op = (IArgumentOperation)oc.Operation; + if (op.Parameter.RefKind != RefKind.None) return; + CheckCopyability(oc, op.Value, ArgumentRule); + }, OperationKind.Argument); + + csc.RegisterOperationAction(oc => + { + var op = (IReturnOperation)oc.Operation; + if (op.ReturnedValue == null) return; + CheckCopyability(oc, op.ReturnedValue, ReturnRule); + }, OperationKind.Return, + OperationKind.YieldReturn); + + csc.RegisterOperationAction(oc => + { + var op = (IConversionOperation)oc.Operation; + var v = op.Operand; + if (v.Kind == OperationKind.DefaultValue) return; + var t = v.Type; + if (!t.IsNonCopyable()) return; + + if (op.OperatorMethod != null && op.OperatorMethod.Parameters.Length == 1) + { + var parameter = op.OperatorMethod.Parameters[0]; + if (parameter.RefKind != RefKind.None) return; + } + + if (op.Parent is IForEachLoopOperation && + op == ((IForEachLoopOperation)op.Parent).Collection && + op.Conversion.IsIdentity) + { + return; + } + + oc.ReportDiagnostic(Error(v.Syntax, ConversionRule, t.Name)); + }, OperationKind.Conversion); + + csc.RegisterOperationAction(oc => + { + var op = (IArrayInitializerOperation)oc.Operation; + + if (!((IArrayTypeSymbol)((IArrayCreationOperation)op.Parent).Type).ElementType.IsNonCopyable()) return; + + foreach (var v in op.ElementValues) + { + CheckCopyability(oc, v, InitializerRule); + } + }, OperationKind.ArrayInitializer); + + csc.RegisterOperationAction(oc => + { + var op = (IDeclarationPatternOperation)oc.Operation; + var t = ((ILocalSymbol)op.DeclaredSymbol).Type; + if (!t.IsNonCopyable()) return; + oc.ReportDiagnostic(Error(op.Syntax, PatternRule, t.Name)); + }, OperationKind.DeclarationPattern); + + csc.RegisterOperationAction(oc => + { + var op = (ITupleOperation)oc.Operation; + + // exclude ParenthesizedVariableDesignationSyntax + if (op.Syntax.Kind() != SyntaxKind.TupleExpression) return; + + foreach (var v in op.Elements) + { + CheckCopyability(oc, v, TupleRule); + } + }, OperationKind.Tuple); + + csc.RegisterOperationAction(oc => + { + // instance property/event should not be referenced with in parameter/ref readonly local/readonly field + var op = (IMemberReferenceOperation)oc.Operation; + CheckInstanceReadonly(oc, op.Instance, MemberRule); + }, OperationKind.PropertyReference, + OperationKind.EventReference); + + csc.RegisterOperationAction(oc => + { + // instance method should not be invoked with in parameter/ref readonly local/readonly field + var op = (IInvocationOperation)oc.Operation; + + CheckGenericConstraints(oc, op, GenericConstraintRule); + CheckInstanceReadonly(oc, op.Instance, ReadOnlyInvokeRule); + + }, OperationKind.Invocation); + + csc.RegisterOperationAction(oc => { + var op = (IDynamicInvocationOperation)oc.Operation; + + foreach(var arg in op.Arguments) { + if (!arg.Type.IsNonCopyable()) continue; + + oc.ReportDiagnostic(Error(arg.Syntax, GenericConstraintRule)); + } + + }, OperationKind.DynamicInvocation); + + csc.RegisterOperationAction(oc => + { + // delagate creation + var op = (IMemberReferenceOperation)oc.Operation; + if (op.Instance == null) return; + if (!op.Instance.Type.IsNonCopyable()) return; + oc.ReportDiagnostic(Error(op.Instance.Syntax, DelegateRule, op.Instance.Type.Name)); + }, OperationKind.MethodReference); + + csc.RegisterSymbolAction(sac => + { + var f = (IFieldSymbol)sac.Symbol; + if (f.IsStatic) return; + if (!f.Type.IsNonCopyable()) return; + if (f.ContainingType.IsReferenceType) return; + if (f.ContainingType.IsNonCopyable()) return; + sac.ReportDiagnostic(Error(f.DeclaringSyntaxReferences[0].GetSyntax(), FieldDeclarationRule, f.Type.Name)); + }, SymbolKind.Field); + }); + + // not supported yet: + // OperationKind.CompoundAssignment, + // OperationKind.UnaryOperator, + // OperationKind.BinaryOperator, + } + + private static void CheckGenericConstraints(in OperationAnalysisContext oc, IInvocationOperation op, DiagnosticDescriptor rule) + { + var m = op.TargetMethod; + + if (m.IsGenericMethod) + { + var parameters = m.TypeParameters; + var arguments = m.TypeArguments; + for (int i = 0; i < parameters.Length; i++) + { + var p = parameters[i]; + var a = arguments[i]; + + if (a.IsNonCopyable() && !p.IsNonCopyable()) + oc.ReportDiagnostic(Error(op.Syntax, rule, a.Name)); + } + } + } + + private static void CheckInstanceReadonly(in OperationAnalysisContext oc, IOperation instance, DiagnosticDescriptor rule) + { + if (instance == null) return; + + var t = instance.Type; + if (!t.IsNonCopyable()) return; + + if (IsInstanceReadonly(instance)) + { + oc.ReportDiagnostic(Error(instance.Syntax, rule, t.Name)); + } + } + + private static Diagnostic Error(SyntaxNode at, DiagnosticDescriptor rule, string name = null) + => name is null + ? Diagnostic.Create(rule, at.GetLocation()) + : Diagnostic.Create(rule, at.GetLocation(), name); + + private static bool IsInstanceReadonly(IOperation instance) + { + bool isReadOnly = false; + switch (instance) + { + case IFieldReferenceOperation r: + isReadOnly = r.Field.IsReadOnly; + break; + case ILocalReferenceOperation r: + isReadOnly = r.Local.RefKind == RefKind.In; + break; + case IParameterReferenceOperation r: + isReadOnly = r.Parameter.RefKind == RefKind.In; + break; + } + + return isReadOnly; + } + + private static bool HasNonCopyableParameter(IMethodSymbol m) + { + foreach (var p in m.Parameters) + { + if(p.RefKind == RefKind.None) + { + if (p.Type.IsNonCopyable()) return true; + } + } + return false; + } + + private static void CheckCopyability(in OperationAnalysisContext oc, IOperation v, DiagnosticDescriptor rule) + { + var t = v.Type; + if (!t.IsNonCopyable()) return; + if (v.CanCopy()) return; + oc.ReportDiagnostic(Error(v.Syntax, rule, t.Name)); + } + } +} diff --git a/src/noncopyable_analyzer/TypeExtensions.cs b/src/noncopyable_analyzer/TypeExtensions.cs new file mode 100644 index 000000000..a1570ad68 --- /dev/null +++ b/src/noncopyable_analyzer/TypeExtensions.cs @@ -0,0 +1,101 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace NonCopyable +{ + public static class TypeExtensions + { + public static bool IsNonCopyable(this ITypeSymbol t) + { + if (t == null) return false; + if (t.TypeKind != TypeKind.Struct && t.TypeKind != TypeKind.TypeParameter) return false; + + if (HasNonCopyableAttribute(t)) return true; + + if (t.TypeKind == TypeKind.Struct) + { + foreach (var ifType in t.AllInterfaces) + { + if (HasNonCopyableAttribute(ifType)) return true; + } + } + else + { + foreach (var constraint in ((ITypeParameterSymbol)t).ConstraintTypes) + { + if (HasNonCopyableAttribute(constraint)) return true; + } + + } + + return false; + } + + private static bool HasNonCopyableAttribute(ITypeSymbol t) + { + foreach (var a in t.GetAttributes()) + { + var str = a.AttributeClass.Name; + if (str.EndsWith("NonCopyable") || str.EndsWith("NonCopyableAttribute")) return true; + } + + return false; + } + + private static SyntaxList GetAttributes(this SyntaxNode syntax) + { + switch (syntax) + { + case StructDeclarationSyntax s: + return s.AttributeLists; + case TypeParameterSyntax s: + return s.AttributeLists; + default: + return default; + } + } + + /// + /// test whether op is copyable or not even when it is a non-copyable instance. + /// + public static bool CanCopy(this IOperation op) + { + var k = op.Kind; + + if (k == OperationKind.Conversion) + { + var operandKind = ((IConversionOperation)op).Operand.Kind; + // default literal (invalid if LangVersion < 7.1) + if (operandKind == OperationKind.DefaultValue || operandKind == OperationKind.Invalid) return true; + } + + if (k == OperationKind.LocalReference || k == OperationKind.FieldReference || k == OperationKind.PropertyReference || k == OperationKind.ArrayElementReference) + { + //need help: how to get ref-ness from IOperation? + var parent = op.Syntax.Parent.Kind(); + if (parent == SyntaxKind.RefExpression) return true; + } + + if (k == OperationKind.Conditional) + { + var cond = (IConditionalOperation)op; + return cond.WhenFalse.CanCopy() && cond.WhenFalse.CanCopy(); + } + + return k == OperationKind.ObjectCreation + || k == OperationKind.DefaultValue + || k == OperationKind.Literal + || k == OperationKind.Invocation + // workaround for https://github.com/dotnet/roslyn/issues/49751 + || !IsValid(k) && op.Syntax is InvocationExpressionSyntax; + + //todo: should return value be OK? + //todo: move semantics + } + + static bool IsValid(OperationKind kind) + => kind != OperationKind.None && kind != OperationKind.Invalid; + } +}