I've already expressed concerns about this type's constructor, and I've sort-of* implemented the changes suggested in the answers I got there.
Recently I added even more parameters to that constructor - namely the ParentDeclaration
and the Annotations
. The Declaration
type is absolutely where these things belong. For example when I come across this:
'@ignore VariableNotUsed
Dim foo As Integer
Then the Declaration
object representing this foo
variable contains an Annotation
string that says @ignore VariableNotUsed
, and the code inspections use that to know whether or not to raise a flag when References
contains no IdentifierReference
that isn't an assignment.
So I'm very much torn right now: I've always seen constructor-bloat as a violation of the Single Responsibility Principle... does this class break SRP?
In Rubberduck's API, a Declaration
is just about anything: a VBAProject, a class or a standard (.bas) module, a form, the controls on a form, variables, constants, enums, enum members, procedures, functions - everything that has an identifier that can be referenced somewhere in code.
I need to clean up that constructor... it's fairly possible that I need to add more members in the future. It seems to me that everything has a purpose and is in its place. What am I not seeing?
Subclassing to chop off a parameter or two completely sounds like an ineffective way of misusing inheritance to solve the wrong problem. Does it?
Declaration.cs
namespace Rubberduck.Parsing.Symbols
{
/// <summary>
/// Defines a declared identifier.
/// </summary>
[DebuggerDisplay("({DeclarationType}) {Accessibility} {IdentifierName} As {AsTypeName} | {Selection}")]
public class Declaration
{
public Declaration(QualifiedMemberName qualifiedName, Declaration parentDeclaration, string parentScope,
string asTypeName, bool isSelfAssigned, bool isWithEvents,
Accessibility accessibility, DeclarationType declarationType, bool isBuiltIn = true, string annotations = null)
:this(qualifiedName, parentDeclaration, parentScope, asTypeName, isSelfAssigned, isWithEvents, accessibility, declarationType, null, Selection.Home, isBuiltIn, annotations)
{}
public Declaration(QualifiedMemberName qualifiedName, Declaration parentDeclaration, string parentScope,
string asTypeName, bool isSelfAssigned, bool isWithEvents,
Accessibility accessibility, DeclarationType declarationType, ParserRuleContext context, Selection selection, bool isBuiltIn = false, string annotations = null)
{
_qualifiedName = qualifiedName;
_parentDeclaration = parentDeclaration;
_parentScope = parentScope;
_identifierName = qualifiedName.MemberName;
_asTypeName = asTypeName;
_isSelfAssigned = isSelfAssigned;
_isWithEvents = isWithEvents;
_accessibility = accessibility;
_declarationType = declarationType;
_selection = selection;
_context = context;
_isBuiltIn = isBuiltIn;
_annotations = annotations;
}
private readonly bool _isBuiltIn;
public bool IsBuiltIn { get { return _isBuiltIn; } }
private readonly Declaration _parentDeclaration;
public Declaration ParentDeclaration { get { return _parentDeclaration; } }
private readonly QualifiedMemberName _qualifiedName;
public QualifiedMemberName QualifiedName { get { return _qualifiedName; } }
private readonly ParserRuleContext _context;
public ParserRuleContext Context { get { return _context; } }
private readonly IList<IdentifierReference> _references = new List<IdentifierReference>();
public IEnumerable<IdentifierReference> References { get { return _references; } }
private readonly string _annotations;
public string Annotations { get { return _annotations; } }
public void AddReference(IdentifierReference reference)
{
if (reference == null || reference.Declaration.Context == reference.Context)
{
return;
}
if (reference.Context.Parent != _context
&& !_references.Select(r => r.Context).Contains(reference.Context.Parent)
&& !_references.Any(r => r.QualifiedModuleName == reference.QualifiedModuleName
&& r.Selection.StartLine == reference.Selection.StartLine
&& r.Selection.EndLine == reference.Selection.EndLine
&& r.Selection.StartColumn == reference.Selection.StartColumn
&& r.Selection.EndColumn == reference.Selection.EndColumn))
{
_references.Add(reference);
}
}
private readonly Selection _selection;
/// <summary>
/// Gets a <c>Selection</c> representing the position of the declaration in the code module.
/// </summary>
/// <remarks>
/// Returns <c>default(Selection)</c> for module identifiers.
/// </remarks>
public Selection Selection { get { return _selection; } }
public QualifiedSelection QualifiedSelection { get { return new QualifiedSelection(_qualifiedName.QualifiedModuleName, _selection); } }
/// <summary>
/// Gets a reference to the VBProject the declaration is made in.
/// </summary>
/// <remarks>
/// This property is intended to differenciate identically-named VBProjects.
/// </remarks>
public VBProject Project { get { return _qualifiedName.QualifiedModuleName.Project; } }
/// <summary>
/// Gets the name of the VBProject the declaration is made in.
/// </summary>
public string ProjectName { get { return _qualifiedName.QualifiedModuleName.ProjectName; } }
/// <summary>
/// Gets the name of the VBComponent the declaration is made in.
/// </summary>
public string ComponentName { get { return _qualifiedName.QualifiedModuleName.ComponentName; } }
private readonly string _parentScope;
/// <summary>
/// Gets the parent scope of the declaration.
/// </summary>
public string ParentScope { get { return _parentScope; } }
private readonly string _identifierName;
/// <summary>
/// Gets the declared name of the identifier.
/// </summary>
public string IdentifierName { get { return _identifierName; } }
private readonly string _asTypeName;
/// <summary>
/// Gets the name of the declared type.
/// </summary>
/// <remarks>
/// This value is <c>null</c> if not applicable,
/// and <c>Variant</c> if applicable but unspecified.
/// </remarks>
public string AsTypeName { get { return _asTypeName; } }
public bool IsArray()
{
if (Context == null)
{
return false;
}
try
{
var declaration = ((dynamic)Context); // Context is AmbiguousIdentifier - parent is the declaration sub-statement where the array parens are
return declaration.LPAREN() != null && declaration.RPAREN() != null;
}
catch (RuntimeBinderException)
{
return false;
}
}
public bool IsTypeSpecified()
{
if (Context == null)
{
return false;
}
try
{
var asType = ((dynamic) Context).asTypeClause() as VBAParser.AsTypeClauseContext;
return asType != null || HasTypeHint();
}
catch (RuntimeBinderException)
{
return false;
}
}
public bool HasTypeHint()
{
string token;
return HasTypeHint(out token);
}
public bool HasTypeHint(out string token)
{
if (Context == null)
{
token = null;
return false;
}
try
{
var hint = ((dynamic)Context).typeHint() as VBAParser.TypeHintContext;
token = hint == null ? null : hint.GetText();
return hint != null;
}
catch (RuntimeBinderException)
{
token = null;
return false;
}
}
public bool IsSelected(QualifiedSelection selection)
{
return QualifiedName.QualifiedModuleName == selection.QualifiedName &&
Selection.ContainsFirstCharacter(selection.Selection);
}
private readonly bool _isSelfAssigned;
/// <summary>
/// Gets a value indicating whether the declaration is a joined assignment (e.g. "As New xxxxx")
/// </summary>
public bool IsSelfAssigned { get { return _isSelfAssigned; } }
private readonly Accessibility _accessibility;
/// <summary>
/// Gets a value specifying the declaration's visibility.
/// This value is used in determining the declaration's scope.
/// </summary>
public Accessibility Accessibility { get { return _accessibility; } }
private readonly DeclarationType _declarationType;
/// <summary>
/// Gets a value specifying the type of declaration.
/// </summary>
public DeclarationType DeclarationType { get { return _declarationType; } }
private readonly bool _isWithEvents;
/// <summary>
/// Gets a value specifying whether the declared type is an event provider.
/// </summary>
/// <remarks>
/// WithEvents declarations are used to identify event handler procedures in a module.
/// </remarks>
public bool IsWithEvents { get { return _isWithEvents; } }
/// <summary>
/// Returns a string representing the scope of an identifier.
/// </summary>
public string Scope
{
get
{
switch (_declarationType)
{
case DeclarationType.Project:
return "VBE";
case DeclarationType.Class:
case DeclarationType.Module:
return _qualifiedName.QualifiedModuleName.ToString();
case DeclarationType.Procedure:
case DeclarationType.Function:
case DeclarationType.PropertyGet:
case DeclarationType.PropertyLet:
case DeclarationType.PropertySet:
return _qualifiedName.QualifiedModuleName + "." + _identifierName;
default:
return _parentScope;
}
}
}
public override bool Equals(object obj)
{
var other = obj as Declaration
if (other == null)
{
return false;
}
return other.IdentifierName == IdentifierName
&& other.ParentDeclaration == ParentDeclaration
&& other.Selection == Selection
&& other.Context == Context
&& other.Project == Project
&& other.QualifiedMemberName == QualifiedMemberName;
}
public override int GetHashCode()
{
return string.Concat(QualifiedName.QualifiedModuleName.ProjectHashCode, ProjectName, ComponentName, _parentScope, _identifierName).GetHashCode();
}
}
}
Does this Equals
implementation comply to the Geneva convention? I feel it's a little bit overkill and could be simplified a bit.
*I said sort-of, because I didn't end up subclassing the Declaration
type for every primitive type out there, and instead of making a BuiltInDeclaration
subclass, I made a ValuedDeclaration
- which I'm using whenever I encounter a Const
declaration. For the built-in declarations, this has the benefit that Rubberduck knows what the built-in values are, which can eventually be leveraged for interestingly smart suggestions (although we're not exactly there yet):
ValuedDeclaration.cs
namespace Rubberduck.Parsing.Symbols
{
public class ValuedDeclaration : Declaration
{
/// <summary>
/// Creates a new valued built-in declaration.
/// </summary>
public ValuedDeclaration(QualifiedMemberName qualifiedName, Declaration parentDeclaration, string parentScope,
string asTypeName, Accessibility accessibility, DeclarationType declarationType, string value)
: this(qualifiedName, parentDeclaration, parentScope, asTypeName, accessibility, declarationType, value, null, Selection.Home, true)
{
}
public ValuedDeclaration(QualifiedMemberName qualifiedName, Declaration parentDeclaration, string parentScope,
string asTypeName, Accessibility accessibility, DeclarationType declarationType, string value,
ParserRuleContext context, Selection selection, bool isBuiltIn = false)
:base(qualifiedName, parentDeclaration, parentScope, asTypeName, true, false, accessibility, declarationType, context, selection, isBuiltIn)
{
_value = value;
}
private readonly string _value;
/// <summary>
/// Gets a string representing the declared value.
/// </summary>
public string Value { get { return _value; } }
}
}
All other feedback welcome.
Declaration
of independance!?! \$\endgroup\$