I would like to use the NDepend API from a Windows PowerShell 5.1 script but may be its not possible due to the fact that Windows PowerShell cannot handle properties with the same name but different casing.
Here is the PowerShell code that works so far:
using namespace NDepend
Set-StrictMode -Version Latest
$AssList = "NDepend.API", "NDepend.Core", "NDepend.Platform.DotNet", "NDepend.Analysis", "NDepend.Platform.DotNet.NetFx", "NDepend.UI.NetFx"
$AssList.ForEach{
$AssPfad = Join-Path -Path "D:\Program Files\NDepend\Lib" -ChildPath "$_.dll"
Add-Type -Path $AssPfad
}
$ndprojPath = "D:\Test1234.ndproj"
# Still using NDepend in the namespace for clarity (although it is not necessary)
$ndprojPathAbsolute = [NDepend.Path.PathHelpers]::ToAbsoluteFilePath($ndprojPath)
$ndProvider = [NDepend.NDependServicesProvider]::new()
$ndManager = $ndProvider.ProjectManager
$project = $ndManager.LoadProject($ndprojPathAbsolute)
$outputDirPath = Join-Path -Path $PSScriptRoot -ChildPath "OutputDir"
$project.Properties.OutputDir = [NDepend.Path.PathHelpers]::ToDirectoryPath($outputDirPath)
$analysisResult = [NDepend.Analysis.ExtensionMethodsProjectAnalysis]::RunAnalysisAndBuildReport($project)
$ndCodebase = $analysisResult.CodeBase
Outputting $ndCodebase leads to an error
format-default : # The field or property "F18r" for type "a.QHkY" differs only in letter casing from the field or property "f18R". The type must be Common Language Specification (CLS) compliant
The rest of the script works and will output the result on an analysis:
$Issues = [Analysis.ExtensionMethodsProjectAnalysis]::ComputeIssues($analysisResult)
$ndIssuesSet = $Issues
$ndIssuesSet.AllRules
At the moment I am not sure how to avoid this error.
PS: The code is only reproducible with a build license.
Update: Thanks to the comment and suggestions by mklement0 I was able to reflect on the property that causes the error
$Ass = [AppDomain]::CurrentDomain.GetAssemblies().Where{$_.GetName().Name -eq "NDepend.Core"}[0]
$t = $ass.gettype("a.QHkY", $True)
# Just for information purpose
$t.GetProperties().Where{$_.Name -match "f18r"}
# Not getting this property with reflection due to casing?
$PropInfo1 = $ass.gettype("a.QHkY", $True).GetProperty("f18r", [System.Reflection.BindingFlags]::IgnoreCase)
# Getting this property
$PropInfo2 = $ass.gettype("a.QHkY", $True).GetProperty("F18r")
# No value since $PropInfo1 is $null
$PropInfo1.GetValue($analysisResult.CodeBase)
# Getting a value
$PropInfo2.GetValue($analysisResult.CodeBase)
But since I want the value of the CodeBase property:
# Get the value of the CodeBase property
$PropInfo = $ass.gettype("a.vHLF", $True).GetProperty("CodeBase")
$Value = $PropInfo.GetValue($ndCodebase)
$Value
I'll get the same format-default error and I am back to square one. $Value contains the value of the CodeBase object but it looks like the PowerShell formatter causes the error.
Update: As mklement0 suggested it boils down to have to "re-create" the object as a custom type and the needed properties.
I did this for anyone who is interested in a solution by using C# code inside the Powershell script. But in the end its probably too much effort.
using namespace NDepend
Set-StrictMode -Version Latest
$AssList = "NDepend.API", "NDepend.Core", "NDepend.Platform.DotNet", "NDepend.Analysis", "NDepend.Platform.DotNet.NetFx", "NDepend.UI.NetFx"
$AssList.ForEach{
$AssPfad = Join-Path -Path "D:\Program Files\NDepend\Lib" -ChildPath "$_.dll"
Add-Type -Path $AssPfad
}
$ndprojPath = "D:\Test.ndproj"
# Still using NDepend in the namespace for clarity (although it is not necessary)
$ndprojPathAbsolute = [Path.PathHelpers]::ToAbsoluteFilePath($ndprojPath)
$ndProvider = [NDepend.NDependServicesProvider]::new()
$ndManager = $ndProvider.ProjectManager
$project = $ndManager.LoadProject($ndprojPathAbsolute)
$analysisResult = [NDepend.Analysis.ExtensionMethodsProjectAnalysis]::RunAnalysis($project)
# Reflection done in C# to avoid dealing with a CodeBase object directly in PowerShell
$CSCode = @"
using NDepend.Analysis;
using NDepend.Issue;
using NDepend.CodeModel;
using System;
using System.Collections.Generic;
using System.Linq;
public class CodeBaseInfo
{
public ICodeBase CodeBase { get; set; }
public int NbLinesOfCode { get; set; }
public CodeBaseInfo(IAnalysisResult analysisResult)
{
this.CodeBase = analysisResult.CodeBase;
var ass = AppDomain.CurrentDomain.GetAssemblies().Where(a=>a.GetName().Name == "NDepend.Core").First();
var propInfo = ass.GetType("a.vHLF").GetProperty("CodeBase");
var propValue = propInfo.GetValue(analysisResult);
propInfo = ass.GetType("a.QHkY").GetProperty("NbLinesOfCode");
this.NbLinesOfCode = Convert.ToInt32(propInfo.GetValue(propValue));
}
// Will result to the format error - only for demonstration purposes
public ICodeBase GetCodeBase()
{
return this.CodeBase;
}
public ICodeBaseView GetApplication() {
return this.CodeBase.Application;
}
public int GetNbLinesOfCode() {
return this.NbLinesOfCode;
}
}
"@
# Please check the path of the NDepend.API.dll
Add-Type -TypeDefinition $CSCode -ReferencedAssemblies "D:\Program Files\NDepend\Lib\NDepend.API.dll", "netstandard"
$CodeBaseInfo = [CodeBaseInfo]::new($analysisResult)
# Same format error
# $CodeBaseInfo.CodeBase
# This works...
$CodeBaseInfo.GetApplication()
$CodeBaseInfo.GetNbLinesOfCode()
Indeed, PowerShell explicitly checks .NET types for having members whose names are case variations of one another (e.g. F18r
vs. f18r
); any such type is by definition not CLS-compliant.
In Windows PowerShell and, up to at least v7.4.x, also in PowerShell (Core) 7, encountering a case variation in property or field names causes a (statement-terminating) error (.NET exception) when access to any of a given type's members is attempted (whereas with respect to methods such case variations are quietly tolerated - assuming there aren't also property / field case variations - but while one of them is invoked, you cannot predictably invoke a given variation).
GitHub issue #10303 is a green-lit proposal to fix this problem, but no one has stepped up to implement it yet.
.F18r
and .f18r
would succeed and access the property / field that matches case-exactly - but a variation that doesn't match exactly will still report an error, e.g. .F18R
.Workaround:
For now, the only way to avoid errors in this context is to use .NET reflection when accessing property and field members that have case variations.
Note that such property and fields may be accessed implicitly (and therefore trigger errors) when outputting instances of their types to the host (display), owing to PowerShell's rich for-display formatting system, which by default represents non-primitive .NET types by their public properties and fields - this is why outputting $ndCodebase
(relying on PowerShell's implicit-output behavior) surfaced an error for you.
You need to know the concrete type of a given instance in order to employ the reflection workaround, because the reflection method must be invoked on the type in order to avoid the error - an interface type may or may not work (whereas invoking any member on an instance - including the .GetType()
method - invariably triggers the error at hand).
.GetType()
on an instance would trigger the error at hand, as it is any member access on an instances that triggers the error (whereas constructing an instance as a whole works fine).In your case, the documentation expresses the property value types only as interfaces, and the interface that the .CodeBase
property value implements seemingly does not encompass the case-variant members that trigger the error.
However, you should be able to determine the concrete type's full type via the intrinsic .pstypenames
property:
[Type] $concretePublicType =
$ndCodebase.pstypenames.Where({ ($_ -as [type]).IsPublic }, 'First')[0]
Using the full, concrete type, you can use reflection to access, for instance, the .F18r
field / property value - case-exactly - as follows, assuming it is a property (if it is a field, replace .GetProperty()
with .GetField()
:
$concretePublicType.GetProperty('F18r').GetValue($ndCodebase)