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

Implement storing of variable definition locations #711

Closed
wants to merge 12 commits into from
6 changes: 1 addition & 5 deletions src/Analysis/Ast/Impl/Analyzer/AnalysisWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,7 @@ public override bool Walk(ExpressionStatement node) {
return false;
}

public override bool Walk(ForStatement node) {
LoopHandler.HandleFor(node);
return base.Walk(node);
}

public override bool Walk(ForStatement node) => LoopHandler.HandleFor(node);
public override bool Walk(FromImportStatement node) => ImportHandler.HandleFromImport(node);
public override bool Walk(GlobalStatement node) => NonLocalHandler.HandleGlobal(node);
public override bool Walk(IfStatement node) => ConditionalHandler.HandleIf(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,19 @@ public IMember LookupNameInScopes(string name, out IScope scope, LookupOptions o
}
} else if (scopes.Count >= 2) {
if (!options.HasFlag(LookupOptions.Nonlocal)) {
// Remove all scopes except global and current scopes.
while (scopes.Count > 2) {
scopes.RemoveAt(1);
}
}

if (!options.HasFlag(LookupOptions.Local)) {
// Remove local scope.
scopes.RemoveAt(0);
}

if (!options.HasFlag(LookupOptions.Global)) {
// Remove global scope.
scopes.RemoveAt(scopes.Count - 1);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Analysis/Ast/Impl/Analyzer/Handlers/AssignmentHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void HandleAssignment(AssignmentStatement node) {
if (Eval.CurrentScope.NonLocals[ne.Name] != null) {
Eval.LookupNameInScopes(ne.Name, out var scope, LookupOptions.Nonlocal);
if (scope != null) {
scope.Variables[ne.Name].Value = value;
scope.Variables[ne.Name].Assign(value, Eval.GetLoc(ne));
} else {
// TODO: report variable is not declared in outer scopes.
}
Expand All @@ -72,7 +72,7 @@ public void HandleAssignment(AssignmentStatement node) {
if (Eval.CurrentScope.Globals[ne.Name] != null) {
Eval.LookupNameInScopes(ne.Name, out var scope, LookupOptions.Global);
if (scope != null) {
scope.Variables[ne.Name].Value = value;
scope.Variables[ne.Name].Assign(value, Eval.GetLoc(ne));
} else {
// TODO: report variable is not declared in global scope.
}
Expand Down
20 changes: 8 additions & 12 deletions src/Analysis/Ast/Impl/Analyzer/Handlers/LoopHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,31 @@ namespace Microsoft.Python.Analysis.Analyzer.Handlers {
internal sealed class LoopHandler : StatementHandler {
public LoopHandler(AnalysisWalker walker) : base(walker) { }

public void HandleFor(ForStatement node) {
public bool HandleFor(ForStatement node) {
var iterable = Eval.GetValueFromExpression(node.List);
var iterator = (iterable as IPythonIterable)?.GetIterator();
if (iterator == null) {
// TODO: report that expression does not evaluate to iterable.
}
var value = iterator?.Next ?? Eval.UnknownType;

switch (node.Left) {
case NameExpression nex:
// for x in y:
Eval.DeclareVariable(nex.Name, value, VariableSource.Declaration, Eval.GetLoc(node.Left));
if (!string.IsNullOrEmpty(nex.Name)) {
Eval.DeclareVariable(nex.Name, value, VariableSource.Declaration, Eval.GetLoc(nex));
}
break;
case TupleExpression tex:
// x = [('abc', 42, True), ('abc', 23, False)]
// for some_str, some_int, some_bool in x:
var names = tex.Items.OfType<NameExpression>().Select(x => x.Name).ToArray();
if (value is IPythonIterable valueIterable) {
var valueIterator = valueIterable.GetIterator();
foreach (var n in names) {
Eval.DeclareVariable(n, valueIterator?.Next ?? Eval.UnknownType, VariableSource.Declaration, Eval.GetLoc(node.Left));
foreach (var nex in tex.Items.OfType<NameExpression>()) {
if (!string.IsNullOrEmpty(nex.Name)) {
Eval.DeclareVariable(nex.Name, value, VariableSource.Declaration, Eval.GetLoc(nex));
}
} else {
// TODO: report that expression yields value that does not evaluate to iterable.
}
break;
}

node.Body?.Walk(Walker);
return false;
}

public void HandleWhile(WhileStatement node) {
Expand Down
15 changes: 14 additions & 1 deletion src/Analysis/Ast/Impl/Values/Definitions/IVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// See the Apache Version 2.0 License for specific language governing
// permissions and limitations under the License.

using System.Collections.Generic;
using Microsoft.Python.Analysis.Types;

namespace Microsoft.Python.Analysis.Values {
Expand All @@ -24,13 +25,25 @@ public interface IVariable: ILocatedMember {
/// Variable name.
/// </summary>
string Name { get; }

/// <summary>
/// Variable source.
/// </summary>
VariableSource Source { get; }

/// <summary>
/// Variable value.
/// </summary>
IMember Value { get; set; }
IMember Value { get; }

/// <summary>
/// Assigns value to the variable.
/// </summary>
void Assign(IMember value, LocationInfo location);

/// <summary>
/// Provides list of all known assignment locations along the path of analysis.
/// </summary>
IReadOnlyList<LocationInfo> Locations { get; }
}
}
19 changes: 18 additions & 1 deletion src/Analysis/Ast/Impl/Values/Variable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@
// See the Apache Version 2.0 License for specific language governing
// permissions and limitations under the License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Python.Analysis.Types;

namespace Microsoft.Python.Analysis.Values {
[DebuggerDisplay("{DebuggerDisplay}")]
internal sealed class Variable : IVariable {
private List<LocationInfo> _locations;

public Variable(string name, IMember value, VariableSource source, LocationInfo location = null) {
Name = name;
Value = value;
Expand All @@ -28,9 +32,22 @@ public Variable(string name, IMember value, VariableSource source, LocationInfo

public string Name { get; }
public VariableSource Source { get; }
public IMember Value { get; set; }
public IMember Value { get; private set; }
public LocationInfo Location { get; }
public PythonMemberType MemberType => PythonMemberType.Variable;
public IReadOnlyList<LocationInfo> Locations => _locations as IReadOnlyList<LocationInfo> ?? new[] { Location };

public void Assign(IMember value, LocationInfo location) {
if (Value == null || Value.GetPythonType().IsUnknown() || value?.GetPythonType().IsUnknown() == false) {
Value = value;
}
AddLocation(location);
}

private void AddLocation(LocationInfo location) {
_locations = _locations ?? new List<LocationInfo> { Location };
_locations.Add(location);
}

private string DebuggerDisplay {
get {
Expand Down
19 changes: 15 additions & 4 deletions src/Analysis/Ast/Impl/Values/VariableCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.Python.Analysis.Types;
using Microsoft.Python.Core.Diagnostics;

namespace Microsoft.Python.Analysis.Values {
[DebuggerDisplay("Count: {Count}")]
internal sealed class VariableCollection : IVariableCollection {
public static readonly IVariableCollection Empty = new VariableCollection();
private readonly ConcurrentDictionary<string, IVariable> _variables = new ConcurrentDictionary<string, IVariable>();
private readonly ConcurrentDictionary<string, Variable> _variables = new ConcurrentDictionary<string, Variable>();

#region ICollection
public int Count => _variables.Count;
Expand All @@ -37,8 +36,16 @@ internal sealed class VariableCollection : IVariableCollection {
#region IVariableCollection
public IVariable this[string name] => _variables.TryGetValue(name, out var v) ? v : null;
public bool Contains(string name) => _variables.ContainsKey(name);
public bool TryGetVariable(string key, out IVariable value) => _variables.TryGetValue(key, out value);
public IReadOnlyList<string> Names => _variables.Keys.ToArray();

public bool TryGetVariable(string key, out IVariable value) {
value = null;
if (_variables.TryGetValue(key, out var v)) {
value = v;
return true;
}
return false;
}
#endregion

#region IMemberContainer
Expand All @@ -48,7 +55,11 @@ internal sealed class VariableCollection : IVariableCollection {

internal void DeclareVariable(string name, IMember value, VariableSource source, LocationInfo location) {
name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentException(nameof(name));
_variables[name] = new Variable(name, value, source, location);
if (_variables.TryGetValue(name, out var existing)) {
existing.Assign(value, location);
} else {
_variables[name] = new Variable(name, value, source, location);
}
}
}
}
109 changes: 109 additions & 0 deletions src/Analysis/Ast/Test/LocationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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.Threading.Tasks;
using FluentAssertions;
using Microsoft.Python.Analysis.Tests.FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TestUtilities;

namespace Microsoft.Python.Analysis.Tests {
[TestClass]
public class LocationTests : 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 Assignments() {
const string code = @"
a = 1
a = 2

for x in y:
a = 3

if True:
a = 4
elif False:
a = 5
else:
a = 6

def func():
a = 0

def func(a):
a = 0

def func():
global a
a = 7

class A:
a: int
";
var analysis = await GetAnalysisAsync(code);
var a = analysis.Should().HaveVariable("a").Which;
a.Locations.Should().HaveCount(7);
a.Locations[0].Span.Should().Be(2, 1, 2, 2);
a.Locations[1].Span.Should().Be(3, 1, 3, 2);
a.Locations[2].Span.Should().Be(6, 5, 6, 6);
a.Locations[3].Span.Should().Be(9, 5, 9, 6);
a.Locations[4].Span.Should().Be(11, 5, 11, 6);
a.Locations[5].Span.Should().Be(13, 5, 13, 6);
a.Locations[6].Span.Should().Be(23, 5, 23, 6);
}

[TestMethod, Priority(0)]
public async Task NonLocal() {
const string code = @"
def outer():
b = 1
def inner():
nonlocal b
b = 2
";
var analysis = await GetAnalysisAsync(code);
var outer = analysis.Should().HaveFunction("outer").Which;
var b = outer.Should().HaveVariable("b").Which;
b.Locations.Should().HaveCount(2);
b.Locations[0].Span.Should().Be(3, 5, 3, 6);
b.Locations[1].Span.Should().Be(6, 9, 6, 10);
}

[TestMethod, Priority(0)]
public async Task FunctionParameter() {
const string code = @"
def func(a):
a = 1
if True:
a = 2
";
var analysis = await GetAnalysisAsync(code);
var outer = analysis.Should().HaveFunction("func").Which;
var a = outer.Should().HaveVariable("a").Which;
a.Locations.Should().HaveCount(3);
a.Locations[0].Span.Should().Be(2, 10, 2, 11);
a.Locations[1].Span.Should().Be(3, 5, 3, 6);
a.Locations[2].Span.Should().Be(5, 9, 5, 10);
}
}
}