Skip to content
New issue

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

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

Already on GitHub? # to your account

Quick fix for access properties by id instead of string #783

Merged
merged 6 commits into from
Sep 25, 2018

Conversation

krasnotsvetov
Copy link
Collaborator

Access properties in Shader, Material, Animator by int is faster than using string overload.

The property IDs created from string hashes are deterministic over the course of a single run, so this quick fix creates a private static readonly field in class and reuse this field for overload.
Example:

public class SimpleTest
{
    public void Method(Material material)
    {
        material.SetFloat("test", 10.0f);
    }
}

is transformed to:

public class SimpleTest
{
    private static readonly int Test = Shader.PropertyToID("test");
    public void Method(Material material)
    {
        material.SetFloat(Test, 10.0f);
    }
}

Copy link
Member

@citizenmatt citizenmatt left a comment

Choose a reason for hiding this comment

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

Looks good to me! Might be worth adding a couple more tests with property names beginning with underscore - I've seen this naming style used a lot in shaders. Make sure the field name get generated correctly, doesn't clash with existing name, etc.

@citizenmatt citizenmatt added this to the Rider 2018.3 milestone Sep 17, 2018
@krasnotsvetov krasnotsvetov force-pushed the 183-vk-qf-properties-by-id branch from 9dd0fc0 to 77c1579 Compare September 19, 2018 17:11
@krasnotsvetov
Copy link
Collaborator Author

Add test with undescore name

@citizenmatt
Copy link
Member

LGTM!

{
var argument = expression.Arguments[argumentIndex];
var (clrName, methodName) = ourTypes[containingType.GetClrName()];
var literal = (argument.Expression as ILiteralExpression)?.ConstantValue.Value as string;
Copy link
Contributor

Choose a reason for hiding this comment

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

What about string concatenation: M("part" + "secondPart") or constant strings: const string constStr = "myString"; M(constStr)?
This code won't work with any of this. I'm not sure whether not supporting them is a deliberate decision or an overlook, please add tests for these scenarios.

public class PreferAddressByIdToGraphicsParamsAnalyzer : UnityElementProblemAnalyzer<IInvocationExpression>
{

private static readonly IDictionary<IClrTypeName, (IClrTypeName, string)> ourTypes = new Dictionary<IClrTypeName, (IClrTypeName, string)>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this IDictionary<TKey, (TKey, TValue)> really necessary? Seems very confusing and any change to this code will require understanding why this type was introduced.
Looks like the only usage var (clrName, methodName) = ourTypes[containingType.GetClrName()]; could be easily rewritten without it:

var clrName = containingType.GetClrName();
var methodName = ourTypes[clrName];

index = 0;
containingType = null;

if (!stringMethod.ShortName.StartsWith("Get") && !stringMethod.ShortName.StartsWith("Set")) return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably worth introducing a variable for stringMethod.ShortName since it's mentioned at least 4 times in this method.

return false;
}

private bool MatchSignatureStringToIntMethod(IMethod stringMethod, IMethod intMethod)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these parameters allow nulls or not?
t.GetDeclaredElement() as IMethod is passed from the predicate filter, so nulls can be passed in,
however the first line uses them without null checks: if (!stringMethod.ReturnType.Equals(intMethod.ReturnType)) return false; so nulls will result in a NullReferenceException.

Annotating members with [NotNul] or [CanBeNull] could have caught possible NREs like this one, please consider using them.

var intMethodParam = intMethodParameters[i];
var stringMethodParam = stringMethodParameters[i];

if (stringMethodParam.Type.IsString() && intMethodParam.Type.IsInt())
Copy link
Contributor

Choose a reason for hiding this comment

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

This allows methods with following signatures:
M(int firstId, int secondId)
M(string firstName, string secondName)
Probaby should only allow one int to string mismatch in parameters.


var factory = CSharpElementFactory.GetInstance(myInvocationExpression);

var fieldInitializerValue = factory.CreateExpression("$0.$1($2)",
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move this code under the if (name == null) as it's not required for other code paths

}

// replace argument
var argument = factory.CreateArgument(ParameterKind.VALUE, factory.CreateReferenceExpression(name));
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this change will break code with named arguments:

    public class InvalidLiteralForProperyNameTest
    {
        public void Test(Material material)
        {
            material.SetFloat(value: 10.0f, name: "f{caret}loat");
        }
    }

Please add tests for such cases



// generate field if we need it. If we have field which addresses to same property reuse it.
var name = TryFindField(myLiteral, classDeclaration, myTypeName, myMapFuntion);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend using declared elements instead of strings. Relying on strings introduces additional bugs since created reference expression loses its context and might be accidently binded to another declared element that hides the desired one, e.g.:

public class TopLevelClass
{
  private static int Test  = Shader.PropertyToID("test");

  public class NestedClass
  {
    private static string Test = "notTest";

    public void Method(Material material)
    {
      material.SetFloat("te{caret}st", 10.0f);
    }
  }
}

I believe this test will use the nested class' Test property which is not correct.

{
if (HasOverloadWithIntParameter(stringMethod, expression, out var argumentIndex, out var containingType))
{
var argument = expression.Arguments[argumentIndex];
Copy link
Contributor

Choose a reason for hiding this comment

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

Please check all the invariants like having at least required number of arguments explicitly.
Current implemenation will work but it just barely misses bugs like a NullReferenceException on an invocation like this:
var s = nameof(Method);.

{
public class InvalidLiteralForProperyNameTest
{
private static readonly int Property = Shader.PropertyToID("Property");
Copy link
Contributor

Choose a reason for hiding this comment

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

material.SetFloat("f{caret}loat", 10.0f);`

is converted to

private static readonly int Property = Shader.PropertyToID("Property");
...
   material.SetFloat({caret}Property, 10.0f);

Are these code samples really equivalent?

string requiredTypeName, string requiredMethodName, out string name)
{
name = null;
foreach (var member in declaration.ClassMemberDeclarations)
Copy link
Contributor

Choose a reason for hiding this comment

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

This code only checks declarations syntaxically located in the passed declaration.
I belive that my original example still won't reuse existing property, could you please add a test for it?

public partial class TestClass
{
  public void Method(Material material)
  {
    material.SetFloat("te{caret}st", 10.0f);
  }
}

public partial class TestClass
{
  private static readonly int Test = Shader.PropertyToID("test");
}

You can check all declarations of a type like this

foreach (var declarationPart in declaration.DeclaredElement.GetDeclarations())

var factory = CSharpElementFactory.GetInstance(myInvocationExpression);

// try find declaration where string to id conversation is done. If we don't find it, create in top-level class
var idDeclaration = TryFindDeclaration(myInvocationExpression, myGraphicsPropertyName, myTypeName, myMapFunction, out var name);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like out var name is always overwritten before usage. Can we get rid of it?

@krasnotsvetov krasnotsvetov force-pushed the 183-vk-qf-properties-by-id branch from c46c70e to 45c2df2 Compare September 25, 2018 09:39
…load and generate field with correct id for the string.
…f constants and literals. Reused fields/properties is searching not only in top-level class. Fix bug with wrong reference. Add tests for all new cases.
@krasnotsvetov krasnotsvetov force-pushed the 183-vk-qf-properties-by-id branch from 45c2df2 to cfb8133 Compare September 25, 2018 09:44
@krasnotsvetov krasnotsvetov merged commit 6fdc3e4 into 183 Sep 25, 2018
@krasnotsvetov krasnotsvetov deleted the 183-vk-qf-properties-by-id branch September 25, 2018 12:32
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants