patterncsharpMinor
Milking a COM type library: "fun" with COM reflection
Viewed 0 times
reflectionwithfuntypemilkinglibrarycom
Problem
Once upon a time, there was a duck that wanted to know where and how user code was calling into the VBA standard library and Excel object model. To match the rest of its API, the poor little duck had to dig through pages and pages and pages of MSDN documentation, and instantiate a
For example, the
```
public static IEnumerable Declarations
{
get
{
if (_standardLibDeclarations == null)
{
var nestedTypes = typeof(VbaStandardLib).GetNestedTypes(BindingFlags.NonPublic).Where(t => Attribute.GetCustomAttribute(t, typeof(CompilerGeneratedAttribute)) == null);
var fields = nestedTypes.SelectMany(t => t.GetFields());
var values = fields.Select(f => f.GetValue(null));
_standardLibDeclarations = values.Cast();
}
return _standardLibDeclarations;
}
}
//...
private class ColorConstantsModule
{
private static readonly QualifiedModuleName ColorConstantsModuleName = new QualifiedModuleName("VBA", "ColorConstants");
public static readonly Declaration ColorConstants = new Declaration(new QualifiedMemberName(ColorConstantsModuleName, "ColorConstants"), VbaLib.Vba, "VBA", "ColorConstants", false, false, Accessibility.Global, DeclarationType.Module);
public static Declaration VbBlack = new ValuedDeclaration(new QualifiedMemberName(ColorConstantsModuleName, "vbBlack"), ColorConstants, "VBA.ColorConstants", "Long", Accessibility.Global, DeclarationType.Constant, "0");
public static Declaration VbBlue = new ValuedDeclaration(new QualifiedMemberName(ColorConstantsModuleName, "vbBlue"), ColorConstants, "VBA.ColorConstants", "Long", Accessibility.Global, Declaration
Declaration object for each and every single module, class, enum, function, property, even and whatnot.For example, the
ColorConstants built-in module from the VBA standard library would be hard-coded as a series of Declaration fields, and provided to rubberduck through a static getter that used reflection to pull all the members:```
public static IEnumerable Declarations
{
get
{
if (_standardLibDeclarations == null)
{
var nestedTypes = typeof(VbaStandardLib).GetNestedTypes(BindingFlags.NonPublic).Where(t => Attribute.GetCustomAttribute(t, typeof(CompilerGeneratedAttribute)) == null);
var fields = nestedTypes.SelectMany(t => t.GetFields());
var values = fields.Select(f => f.GetValue(null));
_standardLibDeclarations = values.Cast();
}
return _standardLibDeclarations;
}
}
//...
private class ColorConstantsModule
{
private static readonly QualifiedModuleName ColorConstantsModuleName = new QualifiedModuleName("VBA", "ColorConstants");
public static readonly Declaration ColorConstants = new Declaration(new QualifiedMemberName(ColorConstantsModuleName, "ColorConstants"), VbaLib.Vba, "VBA", "ColorConstants", false, false, Accessibility.Global, DeclarationType.Module);
public static Declaration VbBlack = new ValuedDeclaration(new QualifiedMemberName(ColorConstantsModuleName, "vbBlack"), ColorConstants, "VBA.ColorConstants", "Long", Accessibility.Global, DeclarationType.Constant, "0");
public static Declaration VbBlue = new ValuedDeclaration(new QualifiedMemberName(ColorConstantsModuleName, "vbBlue"), ColorConstants, "VBA.ColorConstants", "Long", Accessibility.Global, Declaration
Solution
I think you can make this a tiny bit clearer:
You don't actually need to construct the whole group at all. You can use a HashSet instead:
In general a
You can get really fancy and pass in an
Which means you could simply do:
You'll notice that I prefer to keep
e.g.
It's a pain to do it but you'll thank yourself in the long run! Not much of a review... I'll hopefully take a closer look later in the week.
if (!_state.AllDeclarations.Any(declaration => declaration.IsBuiltIn))
{
// multiple projects can (do) have same references; avoid adding them multiple times!
var references = projects.SelectMany(project => project.References.Cast())
.GroupBy(reference => reference.Guid)
.Select(grouping => grouping.First());
foreach (var reference in references)
{
var stopwatch = Stopwatch.StartNew();
var declarations = _comReflector.GetDeclarationsForReference(reference);
foreach (var declaration in declarations)
{
_state.AddDeclaration(declaration);
}
stopwatch.Stop();
Debug.WriteLine("{0} declarations added in {1}ms", reference.Name, stopwatch.ElapsedMilliseconds);
}You don't actually need to construct the whole group at all. You can use a HashSet instead:
var deduper = new HashSet();
var references = projects
.SelectMany(project => project.References.Cast());
foreach (var reference in references)
{
if (!deduper.Add(reference.Guid))
{
continue;
}
// do your stuff.
}In general a
DistinctBy extension method comes in damn handy:public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector == null)
{
throw new ArgumentNullException(nameof(keySelector));
}
var deduper = new HashSet();
return source.Where(item => deduper.Add(keySelector(item)));
}You can get really fancy and pass in an
IEqualityComparer if you want to.Which means you could simply do:
var references = projects
.SelectMany(project => project.References.Cast())
.DistinctBy(r => r.Guid);
foreach (var reference in references)
{You'll notice that I prefer to keep
SelectMany simple and add the call to Cast later. That's a personal preference thing but I find it easier to scan that way. Although that's still true, as you note in the comments, you can't do that here :)GetDeclarationsForReference seems much too long. You should break it up.e.g.
for (var paramIndex = 0; paramIndex < parameterCount; paramIndex++)
{
yield return CreateParameterDeclaration(/* lots of parameters */);
}It's a pain to do it but you'll thank yourself in the long run! Not much of a review... I'll hopefully take a closer look later in the week.
Code Snippets
if (!_state.AllDeclarations.Any(declaration => declaration.IsBuiltIn))
{
// multiple projects can (do) have same references; avoid adding them multiple times!
var references = projects.SelectMany(project => project.References.Cast<Reference>())
.GroupBy(reference => reference.Guid)
.Select(grouping => grouping.First());
foreach (var reference in references)
{
var stopwatch = Stopwatch.StartNew();
var declarations = _comReflector.GetDeclarationsForReference(reference);
foreach (var declaration in declarations)
{
_state.AddDeclaration(declaration);
}
stopwatch.Stop();
Debug.WriteLine("{0} declarations added in {1}ms", reference.Name, stopwatch.ElapsedMilliseconds);
}var deduper = new HashSet<Guid>();
var references = projects
.SelectMany(project => project.References.Cast<Reference>());
foreach (var reference in references)
{
if (!deduper.Add(reference.Guid))
{
continue;
}
// do your stuff.
}public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector == null)
{
throw new ArgumentNullException(nameof(keySelector));
}
var deduper = new HashSet<TKey>();
return source.Where(item => deduper.Add(keySelector(item)));
}var references = projects
.SelectMany(project => project.References.Cast<Reference>())
.DistinctBy(r => r.Guid);
foreach (var reference in references)
{for (var paramIndex = 0; paramIndex < parameterCount; paramIndex++)
{
yield return CreateParameterDeclaration(/* lots of parameters */);
}Context
StackExchange Code Review Q#122112, answer score: 2
Revisions (0)
No revisions yet.