How can I detect when a ReadOnly property has accidentally been passed by reference? - vb.net

I'm working on a project which is written in VB.NET. The project has several Structures which used to have writable fields. I replaced all of those fields with read-only properties, and wrote functions for creating a copy of a structure that has one of its properties changed.
I was assuming that every part of the code that attempted to write to one of these properties would become an error, and then I could simply fix all the errors by making the code call the new functions. To my dismay, it turns out that if a ReadOnly property is accidentally passed into a ByRef parameter of a function, the compiler accepts this with no warning, and the value that's assigned is silently discarded!
Here's an example:
Structure Point
Public ReadOnly Property X As Integer
Public ReadOnly Property Y As Integer
End Structure
Module Module1
Sub IncreaseByOne(ByRef x As Integer)
x = x + 1
End Sub
Sub Main()
Dim point As New Point
IncreaseByOne(point.X)
Console.WriteLine($"point.X is {point.X}")
End Sub
End Module
I was hoping that the line IncreaseByOne(point.X) would throw an error, or at least a warning, since point.X is read-only and it doesn't make sense to pass it by reference. Instead, the code compiles with no warnings, and the value assigned to x inside of IncreaseByOne is silently discarded, and the program prints point.X is 0.
How can I detect all of the places in my code where a read-only property is passed into a function that takes it by reference? The only way I can think of is to go through every read-only property that I have, find all places where that property is used as a parameter, and look to see if that parameter is ByRef. That'll be very time-consuming, but if there's no other solution, then that's what I'll do.
I'm using Visual Studio 2019. I'm open to installing new software in order to do this.

That's really interesting. The VB.NET Compiler really tries to make a property look like a variable. Even if I explicitly declare the property as
Structure Point
Dim _x As Integer
ReadOnly Property X() As Integer
Get
Return _x
End Get
End Property
End Structure
The code compiles and executes as before. If the property setter is added, it even works correctly!
Structure Point
Dim _x As Integer
Property X() As Integer
Get
Return _x
End Get
Set(value As Integer)
_x = value
End Set
End Property
End Structure
With the above change, the program correctly prints 1.
Looking at the generated IL, we can see why:
IL_0009: ldloca.s point
IL_000b: call instance int32 VisualBasicConsoleTest.Point::get_X()
IL_0010: stloc.1 // Store returned value in local variable
IL_0011: ldloca.s // load address of that local variable (and pass to function call)
IL_0013: call void VisualBasicConsoleTest.Program::IncreaseByOne(int32&)
IL_0018: nop
IL_0019: ldloca.s point
IL_001b: ldloc.1 // Load contents of local variable again
IL_001c: call instance void VisualBasicConsoleTest.Point::set_X(int32) // and call setter
Even though we expect an error because a property is not a value (and a byref requires a value), the compiler fakes what we might have intended: He actually generates a call to the getter, stores the value on the stack, passes a reference to the stack(!) to the called function and then calls the setter with that value.
This works in this simple scenario, but I agree with the commenters above, this might be very confusing when looking at it in detail. If the property is actually a computed property, the outcome is just arbitrary (try implementing the getter as Return _x + 1...)
C# would throw an error here, because a property is not a value and hence cannot be used as an out or ref parameter.

As Craig suggested in this answer, I went ahead and wrote a custom analyzer to detect when this occurs. Now, I can simply do Analyze / Run Code Analysis / On Solution, and every place that the described problem occurs gets marked with a warning such as "The property 'point.X' is read-only and should not be passed by reference."
The entire analyzer is available on GitHub. I've copied the important part below:
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SimpleArgument);
}
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
SimpleArgumentSyntax node = (SimpleArgumentSyntax)context.Node;
SemanticModel semanticModel = context.SemanticModel;
if (!IsByRef(node, semanticModel))
return;
(bool isReadOnly, string symbolType) = IsReadOnly(node, semanticModel);
if (isReadOnly)
{
Diagnostic diagnostic = Diagnostic.Create(
Rule,
node.Expression.GetLocation(),
symbolType,
node.Expression.GetText());
context.ReportDiagnostic(diagnostic);
}
}
/// <summary>
/// Determine if the given argument is passed by reference.
/// </summary>
private static bool IsByRef(SimpleArgumentSyntax node, SemanticModel semanticModel)
{
ArgumentListSyntax argumentList = (ArgumentListSyntax)node.Parent;
if (argumentList.Parent is InvocationExpressionSyntax invocation)
{
SymbolInfo functionInfo = semanticModel.GetSymbolInfo(invocation.Expression);
if (functionInfo.Symbol is IMethodSymbol method)
{
IParameterSymbol thisParameter = null;
if (node.IsNamed)
{
thisParameter = method.Parameters.FirstOrDefault(parameter =>
parameter.Name == node.NameColonEquals.Name.ToString());
}
else
{
int thisArgumentIndex = argumentList.Arguments.IndexOf(node);
if (thisArgumentIndex < method.Parameters.Length)
thisParameter = method.Parameters[thisArgumentIndex];
}
// If we couldn't find the parameter for some reason, the
// best we can do is just accept it.
if (thisParameter == null)
return false;
RefKind refKind = thisParameter.RefKind;
if (refKind != RefKind.None && refKind != RefKind.In)
return true;
}
}
return false;
}
/// <summary>
/// Determine if the given argument is a read-only field or property.
/// </summary>
private static (bool isReadOnly, string symbolType) IsReadOnly(SimpleArgumentSyntax node, SemanticModel semanticModel)
{
string symbolType = "field or property";
bool isReadOnly = false;
if (node.Expression is MemberAccessExpressionSyntax memberAccess)
{
SymbolInfo memberInfo = semanticModel.GetSymbolInfo(memberAccess.Name);
if (memberInfo.Symbol is IPropertySymbol propertySymbol && propertySymbol.IsReadOnly)
{
symbolType = "property";
isReadOnly = true;
}
if (memberInfo.Symbol is IFieldSymbol fieldSymbol && fieldSymbol.IsReadOnly)
{
symbolType = "field";
isReadOnly = true;
}
}
return (isReadOnly, symbolType);
}

There isn't a way to catch this with the compiler. Even Option Strict On will allow passing a read-only property to a ByRef argument. This is defined to pass by copy-in/copy-out, and it's surprising to me that the copy-out part will compile even when the Property Set is inaccessible.
If you want to have an automated lint-type check for this, I would imagine that a custom analyzer could find it. I haven't worked with analyzers, so I don't have any specific suggestions for how to write one or set it up.
Otherwise, you're left to a manual check. As was noted in a comment, you can use the "Find All References" command from Visual Studio to help with it, but this will still require a manual review of every read-only property.

Related

Can a CodeAnalysis return a false positive of CA2202? or is really something wrong with my code?

I'm suffering the same issue explained here but iterating a EnvDTE.Processes.
In the question that I linked the user #Plutonix affirms it is a false warning, and I think him reffers to the obj.Getenumerator() mention so I assume my problem will be considered a false warning too, however, if this is a false warning I would like to know more than an affirmation, the arguments to say it is a false warning.
This is the warning:
CA2202 Do not dispose objects multiple times Object
'procs.GetEnumerator()' can be disposed more than once in method
'DebugUtil.GetCurrentVisualStudioInstance()'. To avoid generating a
System.ObjectDisposedException you should not call Dispose more than
one time on an object.: Lines:
214 Elektro.Application.Debugging DebugUtil.vb 214
This is the code, procs object is the involved one on the warning, but I don't see any disposable object:
Public Shared Function GetCurrentVisualStudioInstance() As DTE2
Dim currentInstance As DTE2 = Nothing
Dim processName As String = Process.GetCurrentProcess.MainModule.FileName
Dim instances As IEnumerable(Of DTE2) = DebugUtil.GetVisualStudioInstances
Dim procs As EnvDTE.Processes
For Each instance As DTE2 In instances
procs = instance.Debugger.DebuggedProcesses
For Each p As EnvDTE.Process In procs
If (p.Name = processName) Then
currentInstance = instance
Exit For
End If
Next p
Next instance
Return currentInstance
End Function
PS: Note that the code-block depends on other members but they are unrelevant for this question.
Short version: this looks like a bug in the Code Analysis component to me.
Long version (hey, you suckered me into spending the better part of my afternoon and evening deciphering this, so you might as well spend a little time reading about it :) )…
The first thing I did was look at the IL. Contrary to my guess, it did not contain multiple calls to Dispose() on the same object. So much for that theory.
The method did, however, contain two separate calls to Dispose(), just on different objects. By this time, I was already convinced this was a bug. I've seen mention of CA2202 being triggered when dealing with related classes where one class instance "owns" an instance of the other class, and both instances are disposed. While inconvenient and worth suppressing, the warning seems valid in those cases; one of the objects really is getting disposed of twice.
But in this case, I had two separate IEnumerator objects; one did not own, nor was even related to, the other. Disposing one would not dispose the other. Thus, Code Analysis was wrong to warn about it. But what specifically was confusing it?
After much experimentation, I came up with this near-minimal code example:
Public Class A
Public ReadOnly Property B As B
Get
Return New B
End Get
End Property
End Class
Public Interface IB
Function GetEnumerator() As IEnumerator
End Interface
Public Class B : Implements IB
Public Iterator Function GetEnumerator() As IEnumerator Implements IB.GetEnumerator
Yield New C
End Function
End Class
Public Class C
Dim _value As String
Public Property Value As String
Get
Return _value
End Get
Set(value As String)
_value = value
End Set
End Property
End Class
Public Shared Function GetCurrentVisualStudioInstance2() As A
For Each a As A In GetAs()
For Each c As C In a.B
If (c.Value = Nothing) Then
Return a
End If
Next c
Next a
Return Nothing
End Function
Public Shared Iterator Function GetAs() As IEnumerable(Of A)
Yield New A()
End Function
This produces the same spurious CA2202 you are seeing in the other code example. Interestingly though, a minor change to the declaration and implementation of interface IB causes the warning to go away:
Public Interface IB : Inherits IEnumerable
End Interface
Public Class B : Implements IB
Public Iterator Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Yield New C
End Function
End Class
Somehow, Code Analysis is getting confused by the non-IEnumerable implementation of GetEnumerator(). (Even more weirdly is that the actual type you're using, the Processes interface in the DTE API, both inherits IEnumerable and declares its own GetEnumerator() method…but it's the latter that is the root of the confusion for Code Analysis, not the combination).
With that in hand, I tried to reproduce the issue in C# and found that I could not. I wrote a C# version that was structured exactly as the types and methods in the VB.NET version, but it passed Code Analysis without warnings. So I looked at the IL again.
I found that the C# compiler generates code very similar to, but not exactly the same as, the VB.NET compiler. In particular, for the try/finally blocks that protect the IEnumerator returned for each loop, all of the initialization for those loops is performed outside the try block, while in the VB.NET version the initialization is performed inside.
And apparently, that is also enough to prevent Code Analysis from getting confused about the usage of the disposable objects.
Given that it seems to be the combination of VB.NET's implementation of For Each and the nested loops, one work-around would be to just implement the method differently. I prefer LINQ syntax anyway, and here is a LINQified version of your method that compiles without the Code Analysis warning:
Public Shared Function GetCurrentVisualStudioInstance() As DTE2
Dim processName As String = Process.GetCurrentProcess.MainModule.FileName
Return GetVisualStudioInstances.FirstOrDefault(
Function(instance)
Return instance.Debugger.DebuggedProcesses.Cast(Of EnvDTE.Process).Any(
Function(p)
Return p.Name = processName
End Function)
End Function)
End Function
And for completeness, the C# version (since all of this code started when a C# implementation was converted to VB.NET and then extended to handle the "current instance" case):
public static DTE2 GetCurrentVisualStudioInstance()
{
string processName = Process.GetCurrentProcess().MainModule.FileName;
return GetVisualStudioInstances()
.FirstOrDefault(i => i.Debugger.DebuggedProcesses
.Cast<EnvDTE.Process>().Any(p => p.Name == processName));
}

String.Substring(start, end) sometimes throwing an exception

My program uses a BackgroundWorker to call a PerformAction() method when a different method, Method1 returns true. I also am using the Strategy Pattern to set the correct PerformAction() that should be performed every time the event is raised.
For the Strategy Pattern I created an Abstract Class, and then a class inside the abstract class that inherits it.
Public MustInherit Class Abstract
Public MustOverride Sub PerformAction(ByVal str as String)
Public Class Extends
Inherits Abstract
Public Overrides Sub PerformAction(ByVal str as String)
str = str.Substring(str.IndexOf(":"), str.IndexOf(">"))
MsgBox(str)
End Sub
End Class
I create another class that contains a field of Abstract, and that is used to call PerformAction.
The PerformAction method gets called from the BackgroundWorker.ReportProgress event, which is called when BackgroundWorker.DoWork detects that Method1 is returning true. And with the code above, it causes a System.Reflection.TargetInvocationException with addition information Exception has been thrown by the target of an invocation.
The debugger tells me:
this Cannot obtain value of local or argument '<this>' as it is not available at this instruction pointer, possibly because it has been optimized away. System.Delegate
args Cannot obtain value of local or argument 'args' as it is not available at this instruction pointer, possibly because it has been optimized away. object[]
Strangely enough, when I perform (what seems to me) an identical operation with two substrings:
s = s.Substring(s.IndexOf(":"))
s = s.Substring(0, s.IndexOf(">"))
it functions perfectly.
What is the difference between these two methods? Is my inheritance set up incorrectly and that is what is causing these errors? What's going on here?
Let me know if I need to add more code to explain the situation. Thanks.
To get the effect of
s = s.Substring(s.IndexOf(":"))
s = s.Substring(0, s.IndexOf(">"))
in a single statement, you need to calculate the length of the desired substring
s = s.SubString(s.IndexOf(":"), s.IndexOf(">") - s.IndexOf(":"))
Note that if it is possible that the string does not contain a ":" followed later by a ">", you will need to first verify that IndexOf(":") is >= 0 and that s.IndexOf(">") returns a value greater than IndexOf(":").

Velocity Eventhandler

in velocity, when you do $object.variable if it not be able to find the getter function to
access it or the getter returns a null. it will just show $object.variable explicitly on the page
I know there is a quiet reference, but I don't want to add ! sign to thousands of variables.
I have tried InvalidReferenceEventHandler, NullValueHandler they all didn't get called.
I wander is there a specific type of Eventhandler for this.
Many thanks
The above seems to be a valid choice as well. However here is another option:
public class AppSpecificInvalidReferenceEventHandler implements
InvalidReferenceEventHandler
{
private static final Logger LOGGER =
Logger.getLogger(AppSpecificInvalidReferenceEventHandler.class);
#Override
public Object invalidGetMethod(Context context, String reference,
Object object, String property, Info info)
{
reportInvalidReference(reference, info);
return "";
}
#Override
public boolean invalidSetMethod(Context context, String leftreference,
String rightreference, Info info)
{
reportInvalidReference(leftreference, info);
return false;
}
#Override
public Object invalidMethod(Context context, String reference, Object object,
String method, Info info)
{
if (reference == null) {
reportInvalidReference(object.getClass().getName() + "." + method, info);
} else {
reportInvalidReference(reference, info);
}
return "";
}
private void reportInvalidReference(String reference, Info info)
{
LOGGER.info("REFRERENCE: " + reference + " Info <" + info + ">");
}
}
You'll also need to add the following to your velocity.properties file:
eventhandler.invalidreferences.class=path.to.package.AppSpecificInvalidReferenceEventHandler,org.apache.velocity.app.event.implement.ReportInvalidReferences
You might be surprised at the results though, so it will likely need fine-tuning dependent upon your needs.
I'm basing this off of Engine-1.7 code.
It seems that when an invalid method is called that the utility method EventHandlerUtil.invalidGetMethod is called. This method creates a new InvalidGetMethodExecutor (this is an inner class on InvalidReferenceEventHandler). Eventually this chains down into a call to invalidReferenceHandlerCall which eventually iterates over any handlerIterators which have been defined. Unfortunately I don't know enough about the internals of Velocity to tell you how to inject these values though. My guess is that the user list will suggest a way to override this behavior or a suggestion will be to use / implement a custom tool.
Edit:
According to the Developer Guide you can do the following. You'll need to write some code to deal with it, but it shouldn't be too difficult:
Pluggable Introspection
runtime.introspector.uberspect = org.apache.velocity.util.introspection.UberspectImpl
This property sets the 'Uberspector', the introspection package that handles all introspection strategies for Velocity. You can specify a comma-separated list of Uberspector classes, in which case all Uberspectors are chained. The default chaining behaviour is to return the first non-null value for each introspection call among all provided uberspectors. You can modify this behaviour (for instance to restrict access to some methods) by subclassing org.apache.velocity.util.introspection.AbstractChainableUberspector (or implementing directly org.apache.velocity.util.introspection.ChainableUberspector). This allows you to create more interesting rules or patterns for Uberspection, rather than just returning the first non-null value.

VB.Net why is this not a bug?

I encounter what I believe to be a bug and I was just wondering if this is already known as a issue or if this is not a issue and why.
The problem related to Read Only Properties on a Type when compiling with the VB.Net Compiler in Visual Studio 2008.
Follows are the class definitions and a small C# program which will not compile. (And is correct in not compiling IMHO because the property being set in the Delegate is Read-only)
public interface ITest
{
bool PrivateBool { get; }
}
public class TestClass : ITest
{
bool privateBool = false;
public bool PrivateBool
{
get
{
return privateBool;
}
}
bool publicBool = false;
public bool PublicBool
{
get { return publicBool; }
set { publicBool = value; }
}
}
class Program
{
static void Main(string[] args)
{
TestClass tc = new TestClass();
//Compile Error
//tc.PrivateBool = false;
//Compile Error
//Action act = new Action(delegate()
//{
// tc.PrivateBool = false;
//});
//Action<TestClass> test = new Action<TestClass>(delegate(TestClass tcc)
//{
// tcc.PrivateBool = false;
//});
//Compile Time Error
//Action<TestClass> test = new Action<TestClass>( tz=> tz.PrivateBool = false);
//Compile Time Error
//Action test = new Action(tc.PrivateBool = false);
}
}
In VB.Net However this is a larger issue… the program will compile and execute with no exception. But the property is not set.
This was a nightmare to catch in the debugger at Run time and we feel that the compiler should have caught that we are assigning to a ready only property just as the CSharp compiler alerts you when compiling.
Module Module1
Sub Main()
Dim tc As New TestClass()
Dim setP = New Action(Of TestClass)(Function(d As TestClass) _
d.PrivateBool = False _
)
setP.Invoke(tc)
End Sub
End Module
Can anyone explain if this is correct logic and why?
I assume that someone will respond that the job of the compiler was fulfilled by examining the parameter type to the delegate and that the delegate was typed to accept that parameter just as it should when parsing a Method Body or a Function Body.
My rebuttal to this would be that the compiler DOES throw an error when that property is attempted to be set from within a method but not the delegate. Delegates should be parsed the same as a Method.
Is the C# compiler over extending itself? I think not. My experience is that this is a bug in the vb.net compiler and should be fixed by a patch to the IDE.
Last but surely not least what occurs when the Invoke happens?
The delegate surely does not use reflection to set the property automagically so I assume the CLR sees the read-only qualifier and a NOOP gets executed. Is that actually what occurs or is the behavior undefined?
Thank you for your time!
In VB.NET 2008, there are no statement lambdas. All lambdas are functions. They return a value, not perform an action.
Your VB lambda simply compares d.PrivateBool and False and returns the result of the comparison.
This is not a bug and by design. It is therefore advisable to avoid assigning VB.NET 2008's lambdas to an Action, this is highly confusing for an unprepared person.
Statement lambdas appeared in VB.NET 2010.

Overload DataGridViewCellStyle and give a default value

I'm writing a custom DataGridView object for a large project to hand out to a bunch of developers to make our app sections look consistent.
I want to set defaults for many of the properties of the DataGridView, and I can set many of them like this:
<System.ComponentModel.Browsable(True), System.ComponentModel.DefaultValue(DataGridViewAutoSizeColumnsMode.Fill)>_
Public Overloads Property AutoSizeColumnsMode() As DataGridViewAutoSizeColumnMode
Get
Return MyBase.AutoSizeColumnsMode
End Get
Set(ByVal value As DataGridViewAutoSizeColumnMode)
MyBase.AutoSizeColumnsMode = value
End Set
End Property
These properties overload with their defaults just fine. Its when I started trying to make default Cell styles that I ran into the issue. Since the DataGridViewCellStyle is a class, I cannot make a constant out of it. I've tried changing all of the settings to what I want them to be in the class constructor, and that works great, except that changes made in the designer properties just get set back as soon as the app runs. So putting the changes in the constructor won't do.
Is there anywhere else I can put code that only runs when the control is first dropped on the designer? or any other way of setting a default?
I ran into this problem too. My solution works around the requirement for the DefaultValue argument to be a compile-time constant. I thought, wouldn't it be sufficient to set the value in the class constructor (defined by the static constructor in C#, and the shared constructor in VB) instead?
This seems to be an good work-around in my case, though there are probably instances where it might break since it's not actually present in the meta-data until the class constructor is called upon loading the class, but for a Designer attribute that should be acceptable. Because DefaultValueAttribute.SetValue is protected, I had to define a derived class that makes it public.
This works fine in the designer, it recognizes when the value is the same as the default and omits it from the generated code when possible, and only generates the differences from the default.
Here's the code in C#, this should work in VB too but I'm not overly familiar with its syntax so I'll have to leave that up to you.
public partial class HighlightGrid : DataGridView
{
// Class constructor
static MethodGrid()
{
// Get HighlightStyle attribute handle
DefaultValueSettableAttribute attr =
TypeDescriptor.GetProperties(typeof(HighlightGrid))["HighlightStyle"]
.Attributes[typeof(DefaultValueSettableAttribute)]
as DefaultValueSettableAttribute;
// Set default highlight style
DataGridViewCellStyle style = new DataGridViewCellStyle();
style.BackColor = Color.Chartreuse;
attr.SetValue(style);
}
[DefaultValueSettable, Description("Cell style of highlighted cells")]
public DataGridViewCellStyle HighlightStyle
{
get { return this.highlightStyle; }
set { this.highlightStyle = value; }
}
// ...
}
// Normally the value of DefaultValueAttribute can't be changed and has
// to be a compile-time constant. This derived class allows setting the
// value in the class constructor for example.
public class DefaultValueSettableAttribute : DefaultValueAttribute
{
public DefaultValueSettableAttribute() : base(new object()) { }
public new void SetValue(Object value) { base.SetValue(value); }
}
Actually, I thought about it a while longer and came across a simpler solution for my issue. This does not work for all cases because it relies on the fact that the person using the custom component will likely never want to revert an entire CellStyle back to windows defaults. I ended up comparing a new CellStyle to the current one in the constructor, and only setting the style if they matched. This way it won't overwrite changes, but it will set it up the first time.
Public Class CustomDataGridView
Inherits System.Windows.Forms.DataGridView
Private RowStyle As New DataGridViewCellStyle
Public Sub New()
RowStyle.BackColor = Color.FromArgb(223, 220, 200)
RowStyle.Font = New Font("Arial", 12.75, FontStyle.Bold, GraphicsUnit.Point)
RowStyle.ForeColor = Color.Black
RowStyle.SelectionBackColor = Color.FromArgb(94, 136, 161)
If MyBase.RowsDefaultCellStyle.ToString = (New DataGridViewCellStyle).ToString Then
MyBase.RowsDefaultCellStyle = RowStyle
End If
End Sub
End Class
Just goes to show, Just because you have a golden hammer, doesn't mean that every problem is a nail.