diff --git a/README.md b/README.md index 00433439a..ba346338a 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,16 @@ The following diagnostics are supported: | `inherit-non-class` | Attempted to inherit something that is not a class. | | `too-many-function-arguments` | Too many arguments have been provided to a function call. | | `too-many-positional-arguments-before-star` | Too many arguments have been provided before a starred argument. | +| `no-cls-argument` | First parameter in a class method must be `cls` | +| `no-method-argument` | Method has no arguments +| `no-self-argument` | First parameter in a method must be `self` | `parameter-already-specified` | A argument with this name has already been specified. | | `parameter-missing` | A required positional argument is missing. | | `positional-argument-after-keyword` | A positional argument has been provided after a keyword argument. | | `return-in-init` | Encountered an explicit return in `__init__` function. | | `typing-generic-arguments` | An error occurred while constructing `Generic`. | -| `typing-typevar-arguments` | An error occurred while constructing `TypeVar`. | | `typing-newtype-arguments` | An error occurred while constructing `NewType`. | +| `typing-typevar-arguments` | An error occurred while constructing `TypeVar`. | | `unknown-parameter-name` | The keyword argument name provided is unknown. | | `unresolved-import` | An import cannot be resolved, and may be missing. | | `undefined-variable` | A variable has been used that has not yet been defined. | diff --git a/src/Analysis/Ast/Impl/Analyzer/Symbols/FunctionEvaluator.cs b/src/Analysis/Ast/Impl/Analyzer/Symbols/FunctionEvaluator.cs index 924b2bc73..fb87bb274 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Symbols/FunctionEvaluator.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Symbols/FunctionEvaluator.cs @@ -55,6 +55,7 @@ public FunctionEvaluator(ExpressionEval eval, PythonFunctionOverload overload) 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 @@ -105,6 +106,59 @@ public FunctionEvaluator(ExpressionEval eval, PythonFunctionOverload overload) return annotationType; } + private void CheckValidOverload(IReadOnlyList parameters) { + if (_self?.MemberType == PythonMemberType.Class) { + switch (_function) { + case IPythonFunctionType function: + CheckValidFunction(function, parameters); + break; + //TODO check properties + } + } + } + + private void CheckValidFunction(IPythonFunctionType function, IReadOnlyList parameters) { + // Don't give diagnostics on functions defined in metaclasses + if (_self.IsMetaclass()) { + 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 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) { diff --git a/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs b/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs index 551309c1c..cbd7bdb25 100644 --- a/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs +++ b/src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs @@ -30,5 +30,8 @@ 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"; } } diff --git a/src/Analysis/Ast/Impl/Extensions/BuiltinTypeIdExtensions.cs b/src/Analysis/Ast/Impl/Extensions/BuiltinTypeIdExtensions.cs index 3c9347e0b..583dcc124 100644 --- a/src/Analysis/Ast/Impl/Extensions/BuiltinTypeIdExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/BuiltinTypeIdExtensions.cs @@ -42,7 +42,7 @@ public static string GetTypeName(this BuiltinTypeId id, Version version) public static string GetTypeName(this BuiltinTypeId id, PythonLanguageVersion languageVersion) => id.GetTypeName(languageVersion.IsNone() || languageVersion.Is3x()); - private static string GetTypeName(this BuiltinTypeId id, bool is3x) { + public static string GetTypeName(this BuiltinTypeId id, bool is3x) { string name; switch (id) { case BuiltinTypeId.Bool: name = "bool"; break; diff --git a/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs b/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs index d8705f253..da18512ce 100644 --- a/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/PythonClassExtensions.cs @@ -13,6 +13,7 @@ // 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.Specializations.Typing; using Microsoft.Python.Analysis.Types; @@ -58,6 +59,13 @@ public static class PythonClassExtensions { return unmangledName.StartsWithOrdinal("__") && memberName.EqualsOrdinal($"_{cls.Name}{unmangledName}"); } + /// + /// Returns if the class is a metaclass. Metaclasses have 'type' in their Mro. + /// + public static bool IsMetaclass(this IPythonClassType cls) { + return cls.Mro.Any(b => b.Name == BuiltinTypeId.Type.GetTypeName(true)); + } + /// /// Gets specific type for the given generic type parameter, resolving bounds as well /// diff --git a/src/Analysis/Ast/Impl/Resources.Designer.cs b/src/Analysis/Ast/Impl/Resources.Designer.cs index 3136af9b2..c55b49b5f 100644 --- a/src/Analysis/Ast/Impl/Resources.Designer.cs +++ b/src/Analysis/Ast/Impl/Resources.Designer.cs @@ -267,6 +267,33 @@ internal class Resources { } } + /// + /// Looks up a localized string similar to Class method '{0}' should have 'cls' as first argument.. + /// + internal static string NoClsArgument { + get { + return ResourceManager.GetString("NoClsArgument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Method '{0}' has no argument.. + /// + internal static string NoMethodArgument { + get { + return ResourceManager.GetString("NoMethodArgument", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Method '{0}' should have 'self' as first argument.. + /// + internal static string NoSelfArgument { + get { + return ResourceManager.GetString("NoSelfArgument", resourceCulture); + } + } + /// /// Looks up a localized string similar to property of type {0}. /// diff --git a/src/Analysis/Ast/Impl/Resources.resx b/src/Analysis/Ast/Impl/Resources.resx index 38905e962..d95a64109 100644 --- a/src/Analysis/Ast/Impl/Resources.resx +++ b/src/Analysis/Ast/Impl/Resources.resx @@ -195,6 +195,9 @@ A single constraint to TypeVar is not allowed. + + Method '{0}' should have 'self' as first argument. + The first argument to NewType must be a string. @@ -210,4 +213,10 @@ Inheriting '{0}', which is not a class. - + + Class method '{0}' should have 'cls' as first argument. + + + Method '{0}' has no argument. + + \ No newline at end of file diff --git a/src/Analysis/Ast/Impl/Types/PythonFunctionType.cs b/src/Analysis/Ast/Impl/Types/PythonFunctionType.cs index 044f6c5c0..e28025c76 100644 --- a/src/Analysis/Ast/Impl/Types/PythonFunctionType.cs +++ b/src/Analysis/Ast/Impl/Types/PythonFunctionType.cs @@ -26,6 +26,7 @@ namespace Microsoft.Python.Analysis.Types { [DebuggerDisplay("Function {Name} ({TypeId})")] internal sealed class PythonFunctionType : PythonType, IPythonFunctionType { + private static readonly IReadOnlyList DefaultClassMethods = new[] { "__new__", "__init_subclass__", "__class_getitem__" }; private ImmutableArray _overloads = ImmutableArray.Empty; private bool _isAbstract; private bool _isSpecialized; @@ -71,6 +72,7 @@ Location location location.Module.AddAstNode(this, fd); ProcessDecorators(fd); + DecideClassMethod(); } #region IPythonType @@ -127,9 +129,17 @@ internal void AddOverload(IPythonFunctionOverload overload) return new PythonUnboundMethod(this); } + private void DecideClassMethod() { + if (IsClassMethod) { + return; + } + + IsClassMethod = DefaultClassMethods.Contains(Name); + } + private void ProcessDecorators(FunctionDefinition fd) { + // TODO warn about incompatible combinations, e.g @staticmethod + @classmethod foreach (var dec in (fd.Decorators?.Decorators).MaybeEnumerate().OfType()) { - // TODO: warn about incompatible combinations. switch (dec.Name) { case @"staticmethod": IsStatic = true; diff --git a/src/Analysis/Ast/Test/LintInheritNonClassTests.cs b/src/Analysis/Ast/Test/LintInheritNonClassTests.cs index 19fbfd790..812e8d6be 100644 --- a/src/Analysis/Ast/Test/LintInheritNonClassTests.cs +++ b/src/Analysis/Ast/Test/LintInheritNonClassTests.cs @@ -202,7 +202,7 @@ class B: from module1 import B class C(B): - def hello(): + def hello(self): pass "; diff --git a/src/Analysis/Ast/Test/LintNewTypeTests.cs b/src/Analysis/Ast/Test/LintNewTypeTests.cs index 8060cde6b..9832b97ed 100644 --- a/src/Analysis/Ast/Test/LintNewTypeTests.cs +++ b/src/Analysis/Ast/Test/LintNewTypeTests.cs @@ -100,7 +100,7 @@ public void TestInitialize() from typing import NewType class X: - def hello(): + def hello(self): pass h = X() diff --git a/src/Analysis/Ast/Test/LintNoClsArgumentTests.cs b/src/Analysis/Ast/Test/LintNoClsArgumentTests.cs new file mode 100644 index 000000000..0e8767550 --- /dev/null +++ b/src/Analysis/Ast/Test/LintNoClsArgumentTests.cs @@ -0,0 +1,211 @@ +// 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 System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.Analysis.Tests { + [TestClass] + public class LintNoClsArgumentTests : AnalysisTestBase { + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [TestMethod, Priority(0)] + public async Task FirstArgumentClassMethodNotCls() { + const string code = @" +class Test: + @classmethod + def test(x, y, z): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(4, 14, 4, 15); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task AbstractClassMethodNeedsClsFirstArg() { + const string code = @" +from abc import abstractmethod + +class A: + @classmethod + @abstractmethod + def test(self, x): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(7, 14, 7, 18); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task AbstractClassMethod() { + const string code = @" +from typing import abstractclassmethod +class A: + @abstractclassmethod + def test(cls): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task ClsMethodValidInMetaclass() { + const string code = @" +class A(type): + def x(cls): pass + +class B(A): + def y(cls): pass + +class MyClass(metaclass=B): + pass + +MyClass.x() +MyClass.y() +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentClassMethodSpecialCase() { + const string code = @" +class Test: + def __new__(x, y, z): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(3, 17, 3, 18); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("__new__")); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentNotClsMultiple() { + const string code = @" +class Test: + @classmethod + def test(x, y, z): + pass + + @classmethod + def test2(x, y, z): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(2); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(4, 14, 4, 15); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("test")); + + diagnostic = analysis.Diagnostics.ElementAt(1); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(8, 15, 8, 16); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("test2")); + } + + [TestMethod, Priority(0)] + public async Task NestedClassFuncNoClsArg() { + const string code = @" +class Test: + class Test2: + @classmethod + def hello(x, y, z): + pass + + @classmethod + def test(x, y, z): ... +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(2); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(5, 19, 5, 20); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("hello")); + + diagnostic = analysis.Diagnostics.ElementAt(1); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoClsArgument); + diagnostic.SourceSpan.Should().Be(9, 14, 9, 15); + diagnostic.Message.Should().Be(Resources.NoClsArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentIsCls() { + const string code = @" +class Test: + @classmethod + def test(cls): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentIsClsManyParams() { + const string code = @" +class Test: + @classmethod + def test(cls, a, b, c, d, e): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task NoDiagnosticInMetaclass() { + const string code = @" +class Test(type): + @classmethod + def test(a, b, c, d, e): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + } +} diff --git a/src/Analysis/Ast/Test/LintNoMethodArgumentTests.cs b/src/Analysis/Ast/Test/LintNoMethodArgumentTests.cs new file mode 100644 index 000000000..d83530c29 --- /dev/null +++ b/src/Analysis/Ast/Test/LintNoMethodArgumentTests.cs @@ -0,0 +1,126 @@ +// 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 System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.Analysis.Tests { + [TestClass] + public class LintNoMethodArgumentTests : AnalysisTestBase { + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [TestMethod, Priority(0)] + public async Task MethodNoArgs() { + const string code = @" +class Test: + def test(): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoMethodArgument); + diagnostic.SourceSpan.Should().Be(3, 9, 3, 13); + diagnostic.Message.Should().Be(Resources.NoMethodArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task ClassMethodNoArgs() { + const string code = @" +class Test: + @classmethod + def test(): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoMethodArgument); + diagnostic.SourceSpan.Should().Be(4, 9, 4, 13); + diagnostic.Message.Should().Be(Resources.NoMethodArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task DefaultMethodNoArgs() { + const string code = @" +class Test: + def __init__(): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoMethodArgument); + diagnostic.SourceSpan.Should().Be(3, 9, 3, 17); + diagnostic.Message.Should().Be(Resources.NoMethodArgument.FormatInvariant("__init__")); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentSpace() { + const string code = @" +class Test: + def test( ): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoMethodArgument); + diagnostic.SourceSpan.Should().Be(3, 9, 3, 13); + diagnostic.Message.Should().Be(Resources.NoMethodArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task NoDiagnosticOnStaticMethod() { + const string code = @" +class Test: + @staticmethod + def test(): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task NoDiagnosticInMetaclass() { + const string code = @" +class Test(type): + def test(): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + } +} diff --git a/src/Analysis/Ast/Test/LintNoSelfArgumentTests.cs b/src/Analysis/Ast/Test/LintNoSelfArgumentTests.cs new file mode 100644 index 000000000..abedbd8b3 --- /dev/null +++ b/src/Analysis/Ast/Test/LintNoSelfArgumentTests.cs @@ -0,0 +1,173 @@ +// 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 System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; + +namespace Microsoft.Python.Analysis.Tests { + [TestClass] + public class LintNoSelfArgumentTests : AnalysisTestBase { + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [TestMethod, Priority(0)] + public async Task FirstArgumentMethodNotSelf() { + const string code = @" +class Test: + def test(x, y, z): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoSelfArgument); + diagnostic.SourceSpan.Should().Be(3, 14, 3, 15); + diagnostic.Message.Should().Be(Resources.NoSelfArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentNotSelfMultiple() { + const string code = @" +class Test: + def test(x, y, z): + pass + + def test2(x, y, z): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(2); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoSelfArgument); + diagnostic.SourceSpan.Should().Be(3, 14, 3, 15); + diagnostic.Message.Should().Be(Resources.NoSelfArgument.FormatInvariant("test")); + + diagnostic = analysis.Diagnostics.ElementAt(1); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoSelfArgument); + diagnostic.SourceSpan.Should().Be(6, 15, 6, 16); + diagnostic.Message.Should().Be(Resources.NoSelfArgument.FormatInvariant("test2")); + } + + [TestMethod, Priority(0)] + public async Task NestedClassFuncNoSelfArg() { + const string code = @" +class Test: + class Test2: + def hello(x, y, z): + pass + + def test(x, y, z): ... +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(2); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoSelfArgument); + diagnostic.SourceSpan.Should().Be(4, 19, 4, 20); + diagnostic.Message.Should().Be(Resources.NoSelfArgument.FormatInvariant("hello")); + + diagnostic = analysis.Diagnostics.ElementAt(1); + diagnostic.ErrorCode.Should().Be(ErrorCodes.NoSelfArgument); + diagnostic.SourceSpan.Should().Be(7, 14, 7, 15); + diagnostic.Message.Should().Be(Resources.NoSelfArgument.FormatInvariant("test")); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentIsSelf() { + const string code = @" +class Test: + def test(self): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task FirstArgumentIsSelfManyParams() { + const string code = @" +class Test: + def test(self, a, b, c, d, e): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task StaticMethodNoSelfValid() { + const string code = @" +class C: + @staticmethod + def test(a, b): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task NormalFunction() { + const string code = @" +def test(): + pass + +class C: + def test1(self): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task NormalProperty() { + const string code = @" +class C: + @property + def test(self): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task NoDiagnosticInMetaclass() { + const string code = @" +class Test(type): + def test(a): + pass +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + } +}