Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Give a diagnostic when creating a function in a class and not specifying the first argument as self #1279

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6354e21
Doing preliminary function redefinition check, need to add testS
CTrando Jul 1, 2019
6f334f8
Fixing up error message and cleaning up code
CTrando Jul 1, 2019
2410f5f
Adding some light tests
CTrando Jul 2, 2019
9bbec89
Reordering
CTrando Jul 2, 2019
18779ee
Adding some more tests on function redefinition
CTrando Jul 3, 2019
38c347a
Give diagnostic error when creating functions inside class without se…
CTrando Jul 3, 2019
524d2e8
Merge branch 'master' of github.com:microsoft/python-language-server …
CTrando Jul 16, 2019
ae00ad5
Handling properties, looking over class methods
CTrando Jul 16, 2019
d2abaab
Merge branch 'master' into scratch/NoSelfArgument
CTrando Jul 23, 2019
6e3a97e
Removing redefined test
CTrando Jul 23, 2019
a046e66
Adding no cls diagnostic
CTrando Jul 23, 2019
f6e9c92
Adding no-cls-argument and no-method-argument
CTrando Jul 23, 2019
60ce7c9
Updating comment
CTrando Jul 23, 2019
53962c0
Removing unused error code
CTrando Jul 23, 2019
1aacae6
Handling @staticmethod
CTrando Jul 23, 2019
48f1ff0
Adding edge cases for comma parameters and empty parameter
CTrando Jul 24, 2019
1725532
Merge branch 'master' into scratch/NoSelfArgument
CTrando Aug 8, 2019
49f983c
Allowing cls in metaclass functions
CTrando Aug 8, 2019
23ff7c0
Handling basic invalid decorator combinations
CTrando Aug 12, 2019
f09539a
Minor changes
CTrando Aug 12, 2019
86ea843
Adding a few more tests
CTrando Aug 13, 2019
e0b12a3
More refactors
CTrando Aug 13, 2019
db76f2e
Changing to hashset
CTrando Aug 13, 2019
e9fb7b5
Revert "Changing to hashset"
CTrando Aug 13, 2019
029c12b
Make default class methods static
CTrando Aug 13, 2019
65d4110
Adding extension method for determining if function is __new__ or __i…
CTrando Aug 15, 2019
8f5d5ba
Removing unused extension
CTrando Aug 15, 2019
bac3937
Changing namespace
CTrando Aug 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 108 additions & 5 deletions src/Analysis/Ast/Impl/Analyzer/Symbols/FunctionEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ public override void Evaluate() {
var returnType = TryDetermineReturnValue();

var parameters = Eval.CreateFunctionParameters(_self, _function, FunctionDefinition, !stub);
CheckValidOverload(parameters);
_overload.SetParameters(parameters);

// Do process body of constructors since they may be declaring
// variables that are later used to determine return type of other
// methods and properties.
var ctor = _function.Name.EqualsOrdinal("__init__") || _function.Name.EqualsOrdinal("__new__");
var ctor = _function.IsDunderInit() || _function.IsDunderNew();
if (ctor || returnType.IsUnknown() || Module.ModuleType == ModuleType.User) {
// Return type from the annotation is sufficient for libraries and stubs, no need to walk the body.
FunctionDefinition.Body?.Walk(this);
Expand Down Expand Up @@ -98,6 +99,110 @@ private IPythonType TryDetermineReturnValue() {
return annotationType;
}

private void CheckValidOverload(IReadOnlyList<IParameterInfo> parameters) {
if (_self?.MemberType == PythonMemberType.Class) {
switch (_function) {
case IPythonFunctionType function:
CheckValidFunction(function, parameters);
break;
case IPythonPropertyType property:
CheckValidProperty(property, parameters);
break;
}
}
}

private void CheckValidFunction(IPythonFunctionType function, IReadOnlyList<IParameterInfo> parameters) {
// Only give diagnostic errors on functions if the decorators are valid
if (!function.HasValidDecorators(Eval)) {
return;
}

// Don't give diagnostics on functions defined in metaclasses
if (SelfIsMetaclass()) {
return;
}

// Static methods don't need any diagnostics
if (function.IsStatic) {
return;
}

// Otherwise, functions defined in classes must have at least one argument
if (parameters.IsNullOrEmpty()) {
var funcLoc = Eval.GetLocation(FunctionDefinition.NameExpression);
ReportFunctionParams(Resources.NoMethodArgument, ErrorCodes.NoMethodArgument, funcLoc);
return;
}

var param = parameters[0].Name;
var paramLoc = Eval.GetLocation(FunctionDefinition.Parameters[0]);
// If it is a class method check for cls
if (function.IsClassMethod && !param.Equals("cls")) {
ReportFunctionParams(Resources.NoClsArgument, ErrorCodes.NoClsArgument, paramLoc);
}

// If it is a method check for self
if (!function.IsClassMethod && !param.Equals("self")) {
ReportFunctionParams(Resources.NoSelfArgument, ErrorCodes.NoSelfArgument, paramLoc);
}
}

private void CheckValidProperty(IPythonPropertyType property, IReadOnlyList<IParameterInfo> parameters) {
// Only give diagnostic errors on properties if the decorators are valid
if (!property.HasValidDecorators(Eval)) {
return;
}

// Don't give diagnostics on properties defined in metaclasses
if (SelfIsMetaclass()) {
return;
}

// No diagnostics on static and class properties
if (property.IsStatic || property.IsClassMethod) {
return;
}

// Otherwise, properties defined in classes must have at least one argument
if (parameters.IsNullOrEmpty()) {
var propertyLoc = Eval.GetLocation(FunctionDefinition.NameExpression);
ReportFunctionParams(Resources.NoMethodArgument, ErrorCodes.NoMethodArgument, propertyLoc);
return;
}

var param = parameters[0].Name;
var paramLoc = Eval.GetLocation(FunctionDefinition.Parameters[0]);
// Only check for self on properties because static and class properties are invalid
if (!param.Equals("self")) {
ReportFunctionParams(Resources.NoSelfArgument, ErrorCodes.NoSelfArgument, paramLoc);
}
}

/// <summary>
/// Returns if the function is part of a metaclass definition
/// e.g
/// class A(type):
/// def f(cls): ...
/// f is a metaclass function
/// </summary>
/// <returns></returns>
private bool SelfIsMetaclass() {
// Just allow all specialized types in Mro to avoid false positives
return _self.Mro.Any(b => b.IsSpecialized);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check for Type in the Mro, don't know an easy way besides this

}

private void ReportFunctionParams(string message, string errorCode, LocationInfo location) {
Eval.ReportDiagnostics(
Eval.Module.Uri,
new DiagnosticsEntry(
message.FormatInvariant(FunctionDefinition.Name),
location.Span,
errorCode,
Parsing.Severity.Warning,
DiagnosticSource.Analysis));
}

public override bool Walk(AssignmentStatement node) {
var value = Eval.GetValueFromExpression(node.Right) ?? Eval.UnknownType;
foreach (var lhs in node.Left) {
Expand All @@ -119,10 +224,8 @@ public override bool Walk(ReturnStatement node) {
var value = Eval.GetValueFromExpression(node.Expression);
if (value != null) {
// although technically legal, __init__ in a constructor should not have a not-none return value
if (FunctionDefinition.Name.EqualsOrdinal("__init__") && _function.DeclaringType.MemberType == PythonMemberType.Class
&& !value.IsOfType(BuiltinTypeId.NoneType)) {

Eval.ReportDiagnostics(Module.Uri, new Diagnostics.DiagnosticsEntry(
if (_function.IsDunderInit() && !value.IsOfType(BuiltinTypeId.NoneType)) {
Eval.ReportDiagnostics(Module.Uri, new DiagnosticsEntry(
Resources.ReturnInInit,
node.GetLocation(Eval).Span,
ErrorCodes.ReturnInInit,
Expand Down
9 changes: 4 additions & 5 deletions src/Analysis/Ast/Impl/Analyzer/Symbols/SymbolCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ private void AddOverload(FunctionDefinition fd, IPythonClassMember function, Act
// collection types cannot be determined as imports haven't been processed.
var overload = new PythonFunctionOverload(function, fd, _eval.GetLocationOfName(fd), fd.ReturnAnnotation?.ToCodeString(_eval.Ast));
addOverload(overload);

_table.Add(new FunctionEvaluator(_eval, overload));
}
}
Expand Down Expand Up @@ -165,19 +166,17 @@ private bool TryAddProperty(FunctionDefinition node, IPythonType declaringType)
foreach (var d in decorators.OfType<NameExpression>()) {
switch (d.Name) {
case @"property":
AddProperty(node, declaringType, false);
return true;
case @"abstractproperty":
AddProperty(node, declaringType, true);
AddProperty(node, declaringType);
return true;
}
}
return false;
}

private void AddProperty(FunctionDefinition fd, IPythonType declaringType, bool isAbstract) {
private void AddProperty(FunctionDefinition fd, IPythonType declaringType) {
if (!(_eval.LookupNameInScopes(fd.Name, LookupOptions.Local) is PythonPropertyType existing)) {
existing = new PythonPropertyType(fd, _eval.GetLocationOfName(fd), declaringType, isAbstract);
existing = new PythonPropertyType(fd, _eval.GetLocationOfName(fd), declaringType);
// The variable is transient (non-user declared) hence it does not have location.
// Property type is tracking locations for references and renaming.
_eval.DeclareVariable(fd.Name, existing, VariableSource.Declaration);
Expand Down
4 changes: 4 additions & 0 deletions src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ public static class ErrorCodes {
public const string UndefinedVariable = "undefined-variable";
public const string VariableNotDefinedGlobally= "variable-not-defined-globally";
public const string VariableNotDefinedNonLocal = "variable-not-defined-nonlocal";
public const string NoSelfArgument = "no-self-argument";
public const string NoClsArgument = "no-cls-argument";
public const string NoMethodArgument = "no-method-argument";
public const string ReturnInInit = "return-in-init";
public const string TypingTypeVarArguments = "typing-typevar-arguments";
public const string TypingNewTypeArguments = "typing-newtype-arguments";
public const string TypingGenericArguments = "typing-generic-arguments";
public const string InheritNonClass = "inherit-non-class";
public const string InvalidDecoratorCombination = "invalid-decorator-combination";
}
}
13 changes: 13 additions & 0 deletions src/Analysis/Ast/Impl/Extensions/FunctionDefinitionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.Python.Analysis.Types;

namespace Microsoft.Python.Analysis {
public static class ClassMemberExtensions {
public static bool IsDunderInit(this IPythonClassMember member) {
return member.Name == "__init__" && member.DeclaringType?.MemberType == PythonMemberType.Class;
}

public static bool IsDunderNew(this IPythonClassMember member) {
return member.Name == "__new__" && member.DeclaringType?.MemberType == PythonMemberType.Class;
}
}
}
47 changes: 45 additions & 2 deletions src/Analysis/Ast/Impl/Extensions/PythonFunctionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
// permissions and limitations under the License.

using System.Linq;
using Microsoft.Python.Analysis.Analyzer;
using Microsoft.Python.Analysis.Diagnostics;
using Microsoft.Python.Analysis.Types;
using Microsoft.Python.Analysis.Values;
using Microsoft.Python.Core;
using Microsoft.Python.Parsing;
using Microsoft.Python.Parsing.Ast;

namespace Microsoft.Python.Analysis.Extensions {
public static class PythonFunctionExtensions {
public static bool IsUnbound(this IPythonFunctionType f)
public static bool IsUnbound(this IPythonFunctionType f)
=> f.DeclaringType != null && f.MemberType == PythonMemberType.Function;

public static bool IsBound(this IPythonFunctionType f)
public static bool IsBound(this IPythonFunctionType f)
=> f.DeclaringType != null && f.MemberType == PythonMemberType.Method;

public static bool HasClassFirstArgument(this IPythonClassMember m)
Expand All @@ -34,5 +38,44 @@ public static IScope GetScope(this IPythonFunctionType f) {
IScope gs = f.DeclaringModule.GlobalScope;
return gs?.TraverseBreadthFirst(s => s.Children).FirstOrDefault(s => s.Node == f.FunctionDefinition);
}

/// <summary>
/// Reports any decorator errors on function.
/// Returns true if the decorator combinations for the property are valid
/// </summary>
public static bool HasValidDecorators(this IPythonFunctionType f, IExpressionEvaluator eval) {
bool valid = true;
// If function is abstract, allow all decorators because it will be overridden
if (f.IsAbstract) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this for the abstract check

return valid;
}

foreach (var dec in (f.FunctionDefinition?.Decorators?.Decorators).MaybeEnumerate().OfType<NameExpression>()) {
switch (dec.Name) {
case @"staticmethod":
if (f.IsClassMethod) {
ReportInvalidDecorator(dec, Resources.InvalidDecoratorForFunction.FormatInvariant("Staticmethod", "class"), eval);
valid = false;
}
break;
case @"classmethod":
if (f.IsStatic) {
ReportInvalidDecorator(dec, Resources.InvalidDecoratorForFunction.FormatInvariant("Classmethod", "static"), eval);
valid = false;
}
break;
}
}
return valid;
}

private static void ReportInvalidDecorator(NameExpression name, string errorMsg, IExpressionEvaluator eval) {
eval.ReportDiagnostics(eval.Module.Uri,
new DiagnosticsEntry(
errorMsg, eval.GetLocation(name).Span,
Diagnostics.ErrorCodes.InvalidDecoratorCombination,
Severity.Warning, DiagnosticSource.Analysis
));
}
}
}
60 changes: 60 additions & 0 deletions src/Analysis/Ast/Impl/Extensions/PythonPropertyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright(c) Microsoft Corporation
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the License); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0
//
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
// MERCHANTABILITY OR NON-INFRINGEMENT.
//
// See the Apache Version 2.0 License for specific language governing
// permissions and limitations under the License.

using System.Linq;
using Microsoft.Python.Analysis.Analyzer;
using Microsoft.Python.Analysis.Diagnostics;
using Microsoft.Python.Analysis.Types;
using Microsoft.Python.Core;
using Microsoft.Python.Parsing;
using Microsoft.Python.Parsing.Ast;

namespace Microsoft.Python.Analysis.Extensions {
public static class PythonPropertyExtensions {
/// <summary>
/// Returns true if the decorator combiantions for the property are valid
/// </summary>
public static bool HasValidDecorators(this IPythonPropertyType p, IExpressionEvaluator eval) {
bool valid = true;
// If property is abstract, allow all decorators because it will be overridden
if(p.IsAbstract) {
return valid;
}

foreach (var dec in (p.FunctionDefinition?.Decorators?.Decorators).MaybeEnumerate().OfType<NameExpression>()) {
switch (dec.Name) {
case @"staticmethod":
ReportInvalidDecorator(dec, Resources.InvalidDecoratorForProperty.FormatInvariant("Staticmethods"), eval);
valid = false;
break;
case @"classmethod":
ReportInvalidDecorator(dec, Resources.InvalidDecoratorForProperty.FormatInvariant("Classmethods"), eval);
valid = false;
break;
}
}
return valid;
}

private static void ReportInvalidDecorator(NameExpression name, string errorMsg, IExpressionEvaluator eval) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very similar to the one in PythonFunctionExtensions, any idea how to remove the duplication?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing type, pass actual flags. I.e. HasValidDecorators(bool isAbstract, bool isClassMethod, ...)

eval.ReportDiagnostics(eval.Module.Uri,
new DiagnosticsEntry(
errorMsg, eval.GetLocation(name).Span,
Diagnostics.ErrorCodes.InvalidDecoratorCombination,
Severity.Warning, DiagnosticSource.Analysis
));
}
}
}
3 changes: 0 additions & 3 deletions src/Analysis/Ast/Impl/Extensions/PythonTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,5 @@ public static void TransferDocumentationAndLocation(this IPythonType s, IPythonT
dst.Location = src.Location;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing because was unused and does not check for declaration type and null check for name.

public static bool IsConstructor(this IPythonClassMember m)
=> m.Name.EqualsOrdinal("__init__") || m.Name.EqualsOrdinal("__new__");
}
}
45 changes: 45 additions & 0 deletions src/Analysis/Ast/Impl/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading