Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DYN-1682]: Improve node warning, autocomplete and Node2Code behavior with class name conflicts #10558

Merged
merged 17 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private void AddTypesToCompletionData(string stringToComplete, List<CompletionDa
var partialName = stringToComplete.ToLower();
// Add matching Classes
var classMirrorGroups = StaticMirror.GetAllTypes(core).
Where(x => !x.IsHiddenInLibrary && x.Alias.ToLower().Contains(partialName)).
Where(x => x.Alias.ToLower().Contains(partialName)).
aparajit-pratap marked this conversation as resolved.
Show resolved Hide resolved
GroupBy(x => x.Alias);

foreach (var classMirrorGroup in classMirrorGroups)
Expand All @@ -214,7 +214,7 @@ private void AddTypesToCompletionData(string stringToComplete, List<CompletionDa
}
// Filter out empty types
completions.AddRange(classMirrorGroup.
Where(x => !x.IsEmpty).
Where(x => !x.IsEmpty && !x.IsHiddenInLibrary).
Select(x =>
CompletionData.ConvertMirrorToCompletionData(x, useShorterName,
resolver: resolver)));
Expand Down
142 changes: 85 additions & 57 deletions src/DynamoCore/Engine/ShortestQualifiedNameReplacer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;

using ProtoCore;
using ProtoCore.AST.AssociativeAST;
using ProtoCore.DSASM;
Expand Down Expand Up @@ -90,7 +91,40 @@ public override AssociativeNode VisitIdentifierListNode(IdentifierListNode node)
RightNode = rightNode,
Optr = Operator.dot
};
return leftNode != newLeftNode ? node : RewriteNodeWithShortName(node);
return node;
}

public override AssociativeNode VisitIdentifierNode(IdentifierNode node)
aparajit-pratap marked this conversation as resolved.
Show resolved Hide resolved
{
var name = node.ToString();
string[] strIdentList = name.Split('.');

// return IdentifierNode if identifier string is not separated by '.'
if (strIdentList.Length == 1)
{
return new IdentifierNode(strIdentList[0]);
}

// create IdentifierListNode from string such that
// RightNode is IdentifierNode representing classname
// and LeftNode is IdentifierNode representing one or more namespaces.
var rightNode = new IdentifierNode(strIdentList.Last());
string ident = "";
for (int i = 0; i < strIdentList.Length - 1; i++)
{
if (!string.IsNullOrEmpty(ident))
ident += ".";
ident += strIdentList[i];
}
var leftNode = new IdentifierNode(ident);

var identListNode = new IdentifierListNode
{
LeftNode = leftNode,
RightNode = rightNode,
Optr = Operator.dot
};
return identListNode.Accept(this);
}

private bool TryShortenClassName(IdentifierListNode node, out AssociativeNode shortNameNode)
Expand All @@ -108,84 +142,78 @@ private bool TryShortenClassName(IdentifierListNode node, out AssociativeNode sh
if (matchingClasses.Length == 0)
return false;

string className = qualifiedName.Split('.').Last();

var symbol = new ProtoCore.Namespace.Symbol(qualifiedName);
if (!symbol.Matches(node.ToString()))
if(qualifiedName != node.ToString())
return false;

string className = qualifiedName.Split('.').Last();

shortNameNode = CreateNodeFromShortName(className, qualifiedName);
return shortNameNode != null;
}

private IdentifierListNode RewriteNodeWithShortName(IdentifierListNode node)
private AssociativeNode CreateNodeFromShortName(string className, string qualifiedName)
{
// Get class name from AST
string qualifiedName = CoreUtils.GetIdentifierExceptMethodName(node);
// Get the list of conflicting namespaces that contain the same class name
var matchingClasses = classTable.ClassNodes.Where(x =>
{
//var isHiddenInLibrary = x.ClassAttributes?.HiddenInLibrary ?? false;
aparajit-pratap marked this conversation as resolved.
Show resolved Hide resolved
return x.Name.Split('.').Last().Equals(className) /*&& !isHiddenInLibrary*/;
}).ToList();

// if it is a global method
if (string.IsNullOrEmpty(qualifiedName))
return node;
//var matchingClasses = CoreUtils.GetResolvedClassName(classTable, AstFactory.BuildIdentifier(className));
if (matchingClasses.Count == 0)
return null;

// Make sure qualifiedName is not a property
var lNode = node.LeftNode;
var matchingClasses = classTable.GetAllMatchingClasses(qualifiedName);
while (matchingClasses.Length == 0 && lNode is IdentifierListNode)
if (matchingClasses.Count == 1)
{
qualifiedName = lNode.ToString();
matchingClasses = classTable.GetAllMatchingClasses(qualifiedName);
lNode = ((IdentifierListNode)lNode).LeftNode;
// If there is no class conflict simply use the class name as the shortest name.
return CoreUtils.CreateNodeFromString(className);
}
qualifiedName = lNode.ToString();
string className = qualifiedName.Split('.').Last();

var newIdentList = CreateNodeFromShortName(className, qualifiedName);
if (newIdentList == null)
return node;

// Replace class name in input node with short name (newIdentList)
node = new IdentifierListNode
var shortName = resolver?.LookupShortName(qualifiedName);
if (!string.IsNullOrEmpty(shortName))
{
LeftNode = newIdentList,
RightNode = node.RightNode,
Optr = Operator.dot
};
return node;
}
return CoreUtils.CreateNodeFromString(shortName);
}

private AssociativeNode CreateNodeFromShortName(string className, string qualifiedName)
{
// Get the list of conflicting namespaces that contain the same class name
var matchingClasses = CoreUtils.GetResolvedClassName(classTable, AstFactory.BuildIdentifier(className));
if (matchingClasses.Length == 0)
return null;
// Use the namespace list to derive the list of shortest unique names
var symbolList =
matchingClasses.Select(matchingClass => new Symbol(matchingClass.Name));
var shortNames = Symbol.GetShortestUniqueNames(symbolList);

string shortName;
// if there is no class conflict simply use the class name as the shortest name
if (matchingClasses.Length == 1)
// remove hidden class if any from shortNames before proceeding
var hiddenClassNode = matchingClasses.FirstOrDefault(x => x.ClassAttributes?.HiddenInLibrary ?? false);
if(hiddenClassNode != null)
{
shortName = className;
var keyToRemove = new Symbol(hiddenClassNode.Name);
shortNames.Remove(keyToRemove);
}
else

// Get the shortest name corresponding to the fully qualified name
if(shortNames.TryGetValue(new Symbol(qualifiedName), out shortName))
{
shortName = resolver != null ? resolver.LookupShortName(qualifiedName) : null;
return CoreUtils.CreateNodeFromString(shortName);
}

if (string.IsNullOrEmpty(shortName))
// If shortName for fully qualified classname is not found, it could be a base class
// present in class hierarchy of any of the other matchingClasses, in which case
// set shortName to the one for the derived class.
var qualifiedClassNode = classTable.ClassNodes.FirstOrDefault(x => x.Name == qualifiedName);
var classHierarchies = matchingClasses.Select(y => classTable.GetClassHierarchy(y));
foreach (var hierarchy in classHierarchies)
{
// If A derives from B, which derives from C, the hierarchy for A
// is listed in that order: [A, B, C], so we start searching in reverse order.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

for (int i = hierarchy.Count - 1; i >= 0; i--)
{
// Use the namespace list as input to derive the list of shortest unique names
var symbolList =
matchingClasses.Select(matchingClass => new ProtoCore.Namespace.Symbol(matchingClass))
.ToList();
var shortNames = ProtoCore.Namespace.Symbol.GetShortestUniqueNames(symbolList);

// Get the shortest name corresponding to the fully qualified name
shortName = shortNames[new ProtoCore.Namespace.Symbol(qualifiedName)];
if (hierarchy[i] == qualifiedClassNode)
{
shortName = shortNames[new Symbol(hierarchy[0].Name)];
Copy link
Member

Choose a reason for hiding this comment

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

just double checking - this should always be 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we want the most derived class in that list.

return CoreUtils.CreateNodeFromString(shortName);
}
}
}
// Rewrite the AST using the shortest name
var newIdentList = CoreUtils.CreateNodeFromString(shortName);
return newIdentList;

return null;
}
}
}
11 changes: 9 additions & 2 deletions src/Engine/ProtoCore/ClassTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public class ClassTable
//Symbol table to manage symbols with namespace
private Namespace.SymbolTable symbolTable = new Namespace.SymbolTable();

private List<ClassNode> GetClassHierarchy(ClassNode node)
public List<ClassNode> GetClassHierarchy(ClassNode node)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does internal work here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, since this method is defined in ProtoCore but now being used in DynamoCore.

Copy link
Member

Choose a reason for hiding this comment

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

I understand this is being used by dynamo core - we also can use the internalsVisibleTo attribute to expose internal members to other assemblies which we own.

We should develop this principal further, but a few points.

  • ProtoCore.dll is currently shipped as a part of the DynamoCore nuget package. To me looking at this package, as an external developer I see this as part of the API.
  • A public method in this assembly which is shipped as part of a nuget package, will end up needing to be maintained.

Is this method something that should be used externally by integrators? Is there a compelling reason to make it part of the API? An interesting use case? A workflow that cannot be implemented without it?

If not, then what is the benefit?

If yes, then great, let's add documentation to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have we made a decision on going forward with reducing the API surface of DynamoCore, DynamoCoreWpf etc. by continuing to expose them as our public API or are we deciding on keeping them as-is and making them internal and having a separate API layer for public consumption (based on your RFC)?

Whatever is the strategy, I think then we should come up with certain coding guidelines before we continue to make PR's. Going by the RFC again, it sounds like none of ProtoCore should be part of the API and we should surface just the AST's publicly. I'll make this method internal as well for now.

Copy link
Member

@mjkkirschner mjkkirschner Apr 21, 2020

Choose a reason for hiding this comment

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

No, we have made no decision -very much welcome those discussions - there are many options.
Thanks for addressing this, I don't think we can stop all work before we entirely decide this - but what I am suggesting is that we proceed conservatively until we do so.

I think it's likely we will find that some users are already using ProtoCore, and other binaries we didn't consider fully as APIs.

For example, the Civil3d integration has very specific interactions with callsites and trace, I am not 100% what APIs that team uses to control and redirect trace but I guess it is in protocore.

{
var cNodes = new List<ClassNode> {node};

Expand Down Expand Up @@ -538,10 +538,17 @@ public bool TryGetFullyQualifiedName(string name, out string fullName)
/// <returns>Array of fully qualified name of all matching symbols</returns>
public string[] GetAllMatchingClasses(string name)
{
// First check if there is an exact name match with fully qualified classname
Symbol symbol;
if (symbolTable.TryGetExactSymbol(name, out symbol))
{
if (Constants.kInvalidIndex != symbol.Id)
return new[] {name};
}

var symbols = symbolTable.TryGetSymbols(name, s => s.Matches(name));

var classes = new List<string>();

if (symbols.Length > 1)
{
var baseClass = GetCommonBaseClass(symbols);
Expand Down
20 changes: 19 additions & 1 deletion test/DynamoCoreTests/CodeBlockNodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,24 @@ public void TestCompletionWhenTyping()
Assert.AreEqual(expected, actual);
}

[Test]
[Category("UnitTests")]
public void TestCompletionWithGlobalClassWhenTyping()
{
var codeCompletionServices = new CodeCompletionServices(libraryServicesCore);
string code = "Dupt";
var completions = codeCompletionServices.SearchCompletions(code, Guid.Empty);

// Expected 4 completion items
Assert.AreEqual(3, completions.Count());

string[] expectedValues = {"DupTargetTest", "A.DupTargetTest", "FFITarget.B.DupTargetTest"};
var expected = expectedValues.OrderBy(x => x);
var actual = completions.Select(x => x.Text).OrderBy(x => x);

Assert.AreEqual(expected, actual);
}

[Test]
[Category("UnitTests")]
public void TestMethodKeywordCompletionWhenTyping()
Expand Down Expand Up @@ -2146,7 +2164,7 @@ public void TestHiddenConflictingClassCompletionWhenTyping()
// Expected 1 completion items
Assert.AreEqual(1, completions.Count());

string[] expected = { "AnotherClassWithNameConflict" };
string[] expected = { "FirstNamespace.AnotherClassWithNameConflict" };
var actual = completions.Select(x => x.Text).OrderBy(x => x);

Assert.AreEqual(expected, actual);
Expand Down
78 changes: 70 additions & 8 deletions test/DynamoCoreTests/NodeToCodeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,75 @@ public void TestShortestQualifiedNameReplacerWithStaticProperty()

Assert.IsNotNull(expr1);

// Since there is a conflict with FFITarget.DesignScript.Point and FFITarget.Dynamo.Point,
// node to code generates the shortest unique name, which in this case will be
// Autodesk.Point for Autodesk.DesignScript.Geometry.Point
Assert.AreEqual("ElementResolverTarget.StaticProperty", expr1.RightNode.ToString());
}

[Test]
public void TestShortestQualifiedNameReplacerWithGlobalClass()
{
string libraryPath = "FFITarget.dll";
if (!CurrentDynamoModel.EngineController.LibraryServices.IsLibraryLoaded(libraryPath))
{
CurrentDynamoModel.EngineController.LibraryServices.ImportLibrary(libraryPath);
}

OpenModel(@"core\node2code\ShortenNodeNameWithGlobalClass.dyn");
var nodes = CurrentDynamoModel.CurrentWorkspace.Nodes;
var engine = CurrentDynamoModel.EngineController;

var result = engine.ConvertNodesToCode(nodes, nodes);
result = NodeToCodeCompiler.ConstantPropagationForTemp(result, Enumerable.Empty<string>());
NodeToCodeCompiler.ReplaceWithShortestQualifiedName(engine.LibraryServices.LibraryManagementCore.ClassTable, result.AstNodes);
Assert.IsNotNull(result);
Assert.IsNotNull(result.AstNodes);

var asts = result.AstNodes.ToList();
Assert.AreEqual(2, asts.Count);

var expr1 = asts[0] as BinaryExpressionNode;
var expr2 = asts[1] as BinaryExpressionNode;

Assert.IsNotNull(expr1);
Assert.IsNotNull(expr2);

Assert.AreEqual("DupTargetTest.DupTargetTest()", expr1.RightNode.ToString());
Assert.AreEqual("DupTargetTest.Foo(t1)", expr2.RightNode.ToString());

}

[Test]
public void TestShortestQualifiedNameReplacerWithConflictingClass()
{
string libraryPath = "FFITarget.dll";
if (!CurrentDynamoModel.EngineController.LibraryServices.IsLibraryLoaded(libraryPath))
{
CurrentDynamoModel.EngineController.LibraryServices.ImportLibrary(libraryPath);
}

OpenModel(@"core\node2code\ShortenNodeNameWithConflictingClass.dyn");
var nodes = CurrentDynamoModel.CurrentWorkspace.Nodes;
var engine = CurrentDynamoModel.EngineController;

var result = engine.ConvertNodesToCode(nodes, nodes);
result = NodeToCodeCompiler.ConstantPropagationForTemp(result, Enumerable.Empty<string>());
NodeToCodeCompiler.ReplaceWithShortestQualifiedName(engine.LibraryServices.LibraryManagementCore.ClassTable, result.AstNodes);
Assert.IsNotNull(result);
Assert.IsNotNull(result.AstNodes);

var asts = result.AstNodes.ToList();
Assert.AreEqual(2, asts.Count);

var expr1 = asts[0] as BinaryExpressionNode;
var expr2 = asts[1] as BinaryExpressionNode;

Assert.IsNotNull(expr1);
Assert.IsNotNull(expr2);

Assert.AreEqual("DupTargetTest.DupTargetTest()", expr1.RightNode.ToString());
Assert.AreEqual("DupTargetTest.Prop(t1)", expr2.RightNode.ToString());

}

[Test]
public void TestBasicNode2CodeWorkFlow1()
{
Expand Down Expand Up @@ -957,16 +1020,16 @@ public void TestPropertyToStaticPropertyInvocation()
}

[Test]
public void NonUniqueNamespaceConflict_on_node2code_doesNotCrash()
public void UniqueNamespace_on_node2code_noConflictWarning()
{
string libraryPath = "FFITarget.dll";
if (!CurrentDynamoModel.EngineController.LibraryServices.IsLibraryLoaded(libraryPath))
{
CurrentDynamoModel.EngineController.LibraryServices.ImportLibrary(libraryPath);
}

// this graph contains a single "FFITarget.B.DupTargetTest" node that conflicts non-uniquely with namespace "FFITarget.C.B.DupTargetTest"
OpenModel(@"core\node2code\NonUniqueNamespaceConflict_throwsNodeWarning.dyn");
// this graph contains a single "FFITarget.B.DupTargetTest" node that should not conflict with namespace "FFITarget.C.B.DupTargetTest"
OpenModel(@"core\node2code\UniqueNamespace_on_node2code_noConflictWarning.dyn");
var nodes = CurrentDynamoModel.CurrentWorkspace.Nodes;
SelectAll(nodes);
var command = new DynamoModel.ConvertNodesToCodeCommand();
Expand All @@ -976,8 +1039,7 @@ public void NonUniqueNamespaceConflict_on_node2code_doesNotCrash()
var cbn = nodes.OfType<CodeBlockNodeModel>().FirstOrDefault();
Assert.IsNotNull(cbn);

var error = "Multiple definitions for 'FFITarget.B.DupTargetTest' are found as FFITarget.C.B.DupTargetTest, FFITarget.B.DupTargetTest";
aparajit-pratap marked this conversation as resolved.
Show resolved Hide resolved
Assert.IsTrue(cbn.ToolTipText.Contains(error));
Assert.False(cbn.IsInErrorState);
}


Expand Down
Loading