Restore conditional command exports#329
Conversation
Code Review — PR #329: Restore conditional command exportsOverviewThis PR restores the conditional module-export filtering behavior from the old PSPublishModule. Two root causes are fixed:
The fix introduces two new services ( Potential IssuesMedium —
|
Code Review — PR #329: Restore conditional command exportsOverviewThis PR restores the behaviour where exported functions are conditionally filtered at module import time when an optional dependency module (e.g.
The test additions are well-placed and the overall architecture is clean. Bugs / CorrectnessMedium —
Low — Initially looks like it could be per-module (declared just before Low — The visited-set guard ( PerformanceDouble file-scan when both phases run
The emitted PowerShell (line ~961) calls $PowerForgeAllAliases = @(Get-Alias -ErrorAction SilentlyContinue)
foreach ($PowerForgeFunction in $PowerForgeDependencyCommands) {
...
$PowerForgeAllAliases | Where-Object { $_.Definition -eq $PowerForgeFunction } | ForEach-Object { ... }
}Code QualityHeuristic module detection is fragile for non-standard modules
At line ~180 the caller already trims
Whitespace-only change in Lines 693–708 are a whitespace-only reformat of an existing test. This is harmless but adds noise to the diff. SecurityThe generated PowerShell correctly escapes module names with Test CoverageGood coverage of the new analyzer:
Gaps worth considering:
SummaryThe design is sound, the refactoring is clean, and the test additions are meaningful. The main actionable items are:
Good work on restoring this functionality. The conditional export block builder in particular is a nice, reusable abstraction. |
Code Review — PR #329: Restore conditional command exportsOverviewThis PR fixes a regression where The fix introduces:
The approach is sound. Most of the issues below are minor or medium-impact; nothing is a blocker. IssuesMedium: Double filesystem scan per buildIn var scriptFiles = ModuleMergeComposer.ResolveScriptFiles(buildResult.StagingPath, plan.Information);
var conditionalExportDependencies = ResolveConditionalExportDependencies(plan, scriptFiles, buildResult.Exports);
var mergeInfo = ModuleMergeComposer.BuildSources(...); // calls ResolveScriptFiles again internally
Suggested fix: add an optional Medium:
|
Code Review — PR #329: Restore conditional command exportsOverviewThis PR fixes two related issues:
Overall the implementation is solid — good layering, defensive error handling, and reasonable test coverage. The notes below are a mix of actionable concerns and smaller observations. Correctness
In if (DependsOnModule(function, functions, directModuleHits, new HashSet<string>(StringComparer.OrdinalIgnoreCase)))A new
if (commands.Length == 0)
continue;The
When Performance
This is correctly guarded with a cache, but on a first run against a large module with many unknown external commands, it will spin up a PowerShell instance for each. The calls are sequential and each blocks on Additionally, in a CI environment where optional modules (ActiveDirectory, DhcpServer) are not installed, every Fragility / False-negative risk
The current patterns checked: text.IndexOf("Import-Module ActiveDirectory", StringComparison.OrdinalIgnoreCase) >= 0This will not match:
Since these are heuristics and the PR description calls them "best effort", this is acceptable — but a brief comment listing what is and isn't covered would save a future bug report.
The generic fallback Design / Architecture
internal string[] LocalCalls { get; set; }The two-phase construction (construct → parse all files → backfill
In Generated PowerShellThe generated conditional block is syntactically correct. A few observations:
One edge case: if the same function is listed under two optional modules (e.g., a helper that calls both AD and DNS commands), and only one module is missing, the function is removed entirely because it appears in the first module's Test CoverageGood baseline coverage:
Gaps worth considering:
Minor
SummaryThe core fix (preserving module-only entries in the plan) and the new conditional export generation are sound. The main actionable items are:
Everything else is low-risk polish. |
Code Review: Restore conditional command exportsOverviewThis PR restores a feature where Code Quality & ArchitecturePositives:
Potential Issues1. using var ps = PowerShell.Create();
ps.AddCommand("Get-Command").AddParameter("Name", commandName)...
var result = ps.Invoke();This creates a full PowerShell runspace for every uncached command that isn't covered by naming heuristics or explicit configuration. On modules with many external dependencies that aren't in the well-known list, this could spawn dozens of runspaces during build — each potentially blocking while PowerShell loads. The cache prevents duplicate calls, but the first pass through a large module could be slow. More importantly, this reflects the build host's environment. On a clean CI machine, optional modules won't be present, so 2. Double computation of In var conditionalExportDependencies = plan is null
? new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
: ResolveConditionalExportDependencies(
plan,
ModuleMergeComposer.ResolveScriptFiles(buildResult.StagingPath, plan.Information),
exports);
3. In the generated PowerShell: } catch {
$PowerForgeDependencyCommands = @($PowerForgeCommandModuleDependencies[$PowerForgeDependencyModule])
$PowerForgeAllAliases = @(Get-Alias -ErrorAction SilentlyContinue)
4. Hardcoded module noun-prefix heuristics in if (string.Equals(moduleName, "ActiveDirectory", StringComparison.OrdinalIgnoreCase))
return noun.StartsWith("AD", StringComparison.OrdinalIgnoreCase);
if (string.Equals(moduleName, "DnsServer", StringComparison.OrdinalIgnoreCase))
return noun.StartsWith("DnsServer", StringComparison.OrdinalIgnoreCase);
if (string.Equals(moduleName, "DhcpServer", StringComparison.OrdinalIgnoreCase))
return noun.StartsWith("DhcpServer", StringComparison.OrdinalIgnoreCase);This only applies when 5. if (commands.Length == 0)
continue;After Minor / Style
Test CoverageTests are well-structured and cover the key scenarios:
Missing coverage to consider:
SummaryThe implementation is solid. The core analyzer logic, generated PowerShell, and test coverage are all good quality. The main things worth addressing before merge:
|
Code Review: PR #329 — Restore Conditional Command ExportsOverviewThis PR fixes two related issues:
The solution is well-structured: a new static analyzer ( Code Quality & StyleStrengths
Minor nits
Potential Bugs / Issues1. When a cycle is detected mid-traversal, 2. text.IndexOf("Microsoft.ActiveDirectory.Management", StringComparison.OrdinalIgnoreCase) >= 0A comment like 3. return noun.StartsWith(moduleName, StringComparison.OrdinalIgnoreCase);For a module named 4. Double filesystem enumeration when bootstrapper is regenerated In ModuleMergeComposer.ResolveScriptFiles(buildResult.StagingPath, plan.Information)But Security Considerations
using var ps = PowerShell.Create();
ps.AddCommand("Get-Command").AddParameter("Name", commandName)...
var result = ps.Invoke();
Generated PowerShell variables are injected into module scope The generated block uses
Import-Module -Name $PowerForgeDependencyModule -ErrorAction Stop -Verbose:$falseThis is called in the generated psm1 on every module import. Modules like ActiveDirectory may require domain connectivity at import time. A
private static string EscapePsDoubleQuoted(string value)
=> (value ?? string.Empty).Replace("`", "``").Replace("\"", "`\"");Used to produce the Test Coverage
Gaps worth considering:
Summary
Overall this is a well-reasoned, well-tested fix. The main trade-offs (live |
Code Review — PR #329: Restore conditional command exportsOverviewThis PR restores the historical behaviour of filtering module exports based on optional module availability (ActiveDirectory, DnsServer, DhcpServer). The fix has two parts:
The layering follows AGENTS.md conventions (analyzer in Issues1.
|
Summary
New-ConfigurationCommand -ModuleName ...entries in the module build plan.Root Cause
New-ConfigurationCommandentries without explicit command names were discarded during planning, and the generated C# export block always exported every manifest function. That bypassed the older PSPublishModule behavior that filtered exports when optional modules such as ActiveDirectory, DHCPServer, or DNSServer were unavailable.Validation
dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CommandModuleExportDependencyAnalyzerTests|FullyQualifiedName~ModuleMergeComposerTests|FullyQualifiedName~Plan_KeepsModuleOnlyCommandDependencies|FullyQualifiedName~Run_KeepsCommandModuleDependenciesInPlanWithoutPersistingManifestKey"dotnet build .\PSPublishModule.sln -c Release --no-restoreNote: a broader
PowerForge.Testsrun previously hit an unrelated timeout inPowerForgeCliProjectReleaseTests.ProjectRelease_CliPreservesProjectDefaultsFromConfig; the focused tests above and full solution build passed.