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

Commit 39d7437

Browse files
authored
When a class inherits from something that is not a class, give a diagnostic error (#1277)
* When analyzing class definitions, give diagnostic if bases are not all class types
1 parent 4812d92 commit 39d7437

File tree

6 files changed

+248
-14
lines changed

6 files changed

+248
-14
lines changed

src/Analysis/Ast/Impl/Analyzer/Symbols/ClassEvaluator.cs

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
// See the Apache Version 2.0 License for specific language governing
1414
// permissions and limitations under the License.
1515

16+
using System.Collections;
1617
using System.Collections.Generic;
1718
using System.Linq;
1819
using Microsoft.Python.Analysis.Analyzer.Evaluation;
20+
using Microsoft.Python.Analysis.Diagnostics;
1921
using Microsoft.Python.Analysis.Types;
2022
using Microsoft.Python.Analysis.Values;
2123
using Microsoft.Python.Core;
24+
using Microsoft.Python.Parsing;
2225
using Microsoft.Python.Parsing.Ast;
2326

2427
namespace Microsoft.Python.Analysis.Analyzer.Symbols {
@@ -50,22 +53,12 @@ public void EvaluateClass() {
5053
EvaluateInnerClasses(_classDef);
5154

5255
_class = classInfo;
53-
// Set bases to the class.
54-
var bases = new List<IPythonType>();
55-
foreach (var a in _classDef.Bases.Where(a => string.IsNullOrEmpty(a.Name))) {
56-
// We cheat slightly and treat base classes as annotations.
57-
var b = Eval.GetTypeFromAnnotation(a.Expression);
58-
if (b != null) {
59-
var t = b.GetPythonType();
60-
bases.Add(t);
61-
t.AddReference(Eval.GetLocationOfName(a.Expression));
62-
}
63-
}
64-
_class.SetBases(bases);
6556

57+
var bases = ProcessBases(outerScope);
58+
59+
_class.SetBases(bases);
6660
// Declare __class__ variable in the scope.
6761
Eval.DeclareVariable("__class__", _class, VariableSource.Declaration);
68-
6962
ProcessClassBody();
7063
}
7164
}
@@ -118,6 +111,47 @@ private void ProcessClassBody() {
118111
UpdateClassMembers();
119112
}
120113

114+
private IEnumerable<IPythonType> ProcessBases(Scope outerScope) {
115+
var bases = new List<IPythonType>();
116+
foreach (var a in _classDef.Bases.Where(a => string.IsNullOrEmpty(a.Name))) {
117+
var expr = a.Expression;
118+
119+
switch (expr) {
120+
// class declared in the current module
121+
case NameExpression nameExpression:
122+
var name = Eval.GetInScope(nameExpression.Name, outerScope);
123+
switch (name) {
124+
case PythonClassType classType:
125+
bases.Add(classType);
126+
break;
127+
case IPythonConstant constant:
128+
ReportInvalidBase(constant.Value);
129+
break;
130+
default:
131+
TryAddBase(bases, a);
132+
break;
133+
}
134+
break;
135+
default:
136+
TryAddBase(bases, a);
137+
break;
138+
}
139+
}
140+
return bases;
141+
}
142+
143+
private void TryAddBase(List<IPythonType> bases, Arg arg) {
144+
// We cheat slightly and treat base classes as annotations.
145+
var b = Eval.GetTypeFromAnnotation(arg.Expression);
146+
if (b != null) {
147+
var t = b.GetPythonType();
148+
bases.Add(t);
149+
t.AddReference(Eval.GetLocationOfName(arg.Expression));
150+
} else {
151+
ReportInvalidBase(arg.ToCodeString(Eval.Ast, CodeFormattingOptions.Traditional));
152+
}
153+
}
154+
121155
private void EvaluateConstructors(ClassDefinition cd) {
122156
// Do not use foreach since walker list is dynamically modified and walkers are removed
123157
// after processing. Handle __init__ and __new__ first so class variables are initialized.
@@ -152,6 +186,17 @@ private void UpdateClassMembers() {
152186
_class.AddMembers(members, false);
153187
}
154188

189+
private void ReportInvalidBase(object argVal) {
190+
Eval.ReportDiagnostics(Eval.Module.Uri,
191+
new DiagnosticsEntry(
192+
Resources.InheritNonClass.FormatInvariant(argVal),
193+
_classDef.NameExpression.GetLocation(Eval)?.Span ?? default,
194+
Diagnostics.ErrorCodes.InheritNonClass,
195+
Severity.Error,
196+
DiagnosticSource.Analysis
197+
));
198+
}
199+
155200
// Classes and functions are walked by their respective evaluators
156201
public override bool Walk(ClassDefinition node) => false;
157202
public override bool Walk(FunctionDefinition node) => false;

src/Analysis/Ast/Impl/Diagnostics/ErrorCodes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ public static class ErrorCodes {
2929
public const string ReturnInInit = "return-in-init";
3030
public const string TypingNewTypeArguments = "typing-newtype-arguments";
3131
public const string TypingGenericArguments = "typing-generic-arguments";
32+
public const string InheritNonClass = "inherit-non-class";
3233
}
3334
}

src/Analysis/Ast/Impl/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analysis/Ast/Impl/Resources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,7 @@
204204
<data name="GenericNotAllUnique" xml:space="preserve">
205205
<value>Arguments to Generic must all be unique.</value>
206206
</data>
207-
</root>
207+
<data name="InheritNonClass" xml:space="preserve">
208+
<value>Inheriting '{0}', which is not a class.</value>
209+
</data>
210+
</root>

src/Analysis/Ast/Impl/Types/PythonClassType.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System.Collections.Generic;
1818
using System.Diagnostics;
1919
using System.Linq;
20+
using Microsoft.Python.Analysis.Analyzer;
21+
using Microsoft.Python.Analysis.Diagnostics;
2022
using Microsoft.Python.Analysis.Modules;
2123
using Microsoft.Python.Analysis.Specializations.Typing;
2224
using Microsoft.Python.Analysis.Types.Collections;
@@ -190,6 +192,7 @@ internal void SetBases(IEnumerable<IPythonType> bases) {
190192
}
191193

192194
bases = bases != null ? bases.Where(b => !b.GetPythonType().IsUnknown()).ToArray() : Array.Empty<IPythonType>();
195+
193196
// For Python 3+ attach object as a base class by default except for the object class itself.
194197
if (DeclaringModule.Interpreter.LanguageVersion.Is3x() && DeclaringModule.ModuleType != ModuleType.Builtins) {
195198
var objectType = DeclaringModule.Interpreter.GetBuiltinType(BuiltinTypeId.Object);
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright(c) Microsoft Corporation
2+
// All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
5+
// this file except in compliance with the License. You may obtain a copy of the
6+
// License at http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
9+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
10+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
11+
// MERCHANTABILITY OR NON-INFRINGEMENT.
12+
//
13+
// See the Apache Version 2.0 License for specific language governing
14+
// permissions and limitations under the License.
15+
16+
using System.Linq;
17+
using System.Threading.Tasks;
18+
using FluentAssertions;
19+
using Microsoft.Python.Analysis.Diagnostics;
20+
using Microsoft.Python.Analysis.Tests.FluentAssertions;
21+
using Microsoft.Python.Core;
22+
using Microsoft.Python.Parsing.Tests;
23+
using Microsoft.VisualStudio.TestTools.UnitTesting;
24+
using TestUtilities;
25+
26+
namespace Microsoft.Python.Analysis.Tests {
27+
[TestClass]
28+
public class InheritNonClassTests : AnalysisTestBase {
29+
public TestContext TestContext { get; set; }
30+
31+
[TestInitialize]
32+
public void TestInitialize()
33+
=> TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}");
34+
35+
[TestCleanup]
36+
public void Cleanup() => TestEnvironmentImpl.TestCleanup();
37+
38+
[TestMethod, Priority(0)]
39+
public async Task InheritFromRenamedBuiltin() {
40+
const string code = @"
41+
tmp = str
42+
43+
class C(tmp):
44+
def method(self):
45+
return 'test'
46+
";
47+
var analysis = await GetAnalysisAsync(code);
48+
analysis.Diagnostics.Should().BeEmpty();
49+
}
50+
51+
52+
[TestMethod, Priority(0)]
53+
public async Task InheritFromBuiltin() {
54+
const string code = @"
55+
class C(str):
56+
def method(self):
57+
return 'test'
58+
";
59+
var analysis = await GetAnalysisAsync(code);
60+
analysis.Diagnostics.Should().BeEmpty();
61+
}
62+
63+
64+
[TestMethod, Priority(0)]
65+
public async Task InheritFromUserClass() {
66+
const string code = @"
67+
class D:
68+
def hello(self):
69+
pass
70+
71+
class C(D):
72+
def method(self):
73+
return 'test'
74+
";
75+
var analysis = await GetAnalysisAsync(code);
76+
analysis.Diagnostics.Should().BeEmpty();
77+
}
78+
79+
80+
[TestMethod, Priority(0)]
81+
public async Task InheritFromConstant() {
82+
const string code = @"
83+
class C(5):
84+
def method(self):
85+
return 'test'
86+
";
87+
var analysis = await GetAnalysisAsync(code);
88+
analysis.Diagnostics.Should().HaveCount(1);
89+
90+
var diagnostic = analysis.Diagnostics.ElementAt(0);
91+
diagnostic.SourceSpan.Should().Be(2, 7, 2, 8);
92+
diagnostic.Message.Should().Be(Resources.InheritNonClass.FormatInvariant("5"));
93+
diagnostic.ErrorCode.Should().Be(ErrorCodes.InheritNonClass);
94+
}
95+
96+
97+
[TestMethod, Priority(0)]
98+
public async Task InheritFromConstantVar() {
99+
const string code = @"
100+
x = 'str'
101+
102+
class C(x):
103+
def method(self):
104+
return 'test'
105+
106+
x = 5
107+
108+
class D(x):
109+
def method(self):
110+
return 'test'
111+
";
112+
var analysis = await GetAnalysisAsync(code);
113+
analysis.Diagnostics.Should().HaveCount(2);
114+
115+
var diagnostic = analysis.Diagnostics.ElementAt(0);
116+
diagnostic.SourceSpan.Should().Be(4, 7, 4, 8);
117+
diagnostic.Message.Should().Be(Resources.InheritNonClass.FormatInvariant("str"));
118+
diagnostic.ErrorCode.Should().Be(ErrorCodes.InheritNonClass);
119+
120+
diagnostic = analysis.Diagnostics.ElementAt(1);
121+
diagnostic.SourceSpan.Should().Be(10, 7, 10, 8);
122+
diagnostic.Message.Should().Be(Resources.InheritNonClass.FormatInvariant("5"));
123+
diagnostic.ErrorCode.Should().Be(ErrorCodes.InheritNonClass);
124+
}
125+
126+
[Ignore]
127+
[TestMethod, Priority(0)]
128+
public async Task InheritFromBinaryOp() {
129+
const string code = @"
130+
x = 5
131+
132+
class C(x + 2):
133+
def method(self):
134+
return 'test'
135+
";
136+
var analysis = await GetAnalysisAsync(code);
137+
analysis.Diagnostics.Should().HaveCount(1);
138+
139+
var diagnostic = analysis.Diagnostics.ElementAt(0);
140+
diagnostic.SourceSpan.Should().Be(4, 7, 4, 8);
141+
diagnostic.Message.Should().Be(Resources.InheritNonClass.FormatInvariant("x + 2"));
142+
diagnostic.ErrorCode.Should().Be(ErrorCodes.InheritNonClass);
143+
}
144+
145+
146+
[TestMethod, Priority(0)]
147+
public async Task InheritFromOtherModule() {
148+
const string code = @"
149+
import typing
150+
151+
class C(typing.TypeVar):
152+
def method(self):
153+
return 'test'
154+
";
155+
var analysis = await GetAnalysisAsync(code);
156+
analysis.Diagnostics.Should().BeEmpty();
157+
}
158+
159+
[TestMethod, Priority(0)]
160+
public async Task InheritFromRenamedOtherModule() {
161+
const string code = @"
162+
import typing
163+
164+
tmp = typing.TypeVar
165+
class C(tmp):
166+
def method(self):
167+
return 'test'
168+
";
169+
var analysis = await GetAnalysisAsync(code);
170+
analysis.Diagnostics.Should().BeEmpty();
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)