|
| 1 | +--- |
| 2 | +name: code-review |
| 3 | +description: Review changes to PnP PowerShell for the failure modes this repository actually ships - silently ignored input, unpaged Graph collections, the wrong base class, permission attributes that do not match the API called, culture and cross-platform bugs, and missing documentation or changelog updates. Use when reviewing a diff, a PR, or uncommitted changes. |
| 4 | +--- |
| 5 | + |
| 6 | +# Playbook: code-review |
| 7 | + |
| 8 | +Review changes to PnP PowerShell for the failure modes this repository actually ships. |
| 9 | + |
| 10 | +Cmdlets here run unattended, against production tenants, often with tenant-wide permissions. A cmdlet |
| 11 | +that quietly does the wrong thing is worse than one that fails, because nobody finds out until the |
| 12 | +tenant is already changed. Weight findings accordingly. |
| 13 | + |
| 14 | +Verify before reporting. A claim about behaviour that nobody ran is a guess; say so when it is one. |
| 15 | + |
| 16 | +Language and API rules live in [`dotnet-standards`](../dotnet-standards/SKILL.md). This playbook is about |
| 17 | +what goes wrong here specifically. |
| 18 | + |
| 19 | +> Report to the user, in the session. **Never post a review, a comment, or an approval to GitHub**, |
| 20 | +> and never open an issue for a finding — see |
| 21 | +> [Human in the loop](../../../AGENTS.md#human-in-the-loop). |
| 22 | +
|
| 23 | +## Layout |
| 24 | + |
| 25 | +- `src/Commands/` — cmdlet implementations, one folder per feature area |
| 26 | +- `src/Commands/Base/` — base classes, `PipeBinds/` for parameter binding types |
| 27 | +- `src/Commands/Attributes/` — permission and behaviour attributes |
| 28 | +- `src/ALC/` — assembly load context that isolates private dependencies |
| 29 | +- `documentation/<Verb-PnPNoun>.md` — the reference page for every cmdlet, one file each |
| 30 | +- `pages/articles/` — conceptual articles, listed in `pages/articles/toc.yml` |
| 31 | +- `pages/_site/` — generated site output, never edited by hand |
| 32 | +- `build/` — build and generator scripts |
| 33 | +- `CHANGELOG.md` — release notes |
| 34 | + |
| 35 | +Much of the provisioning behaviour lives in **PnP Framework**, a separate repository. When a root |
| 36 | +cause sits there, say so rather than accepting a workaround layered on top here, and do not let a PR |
| 37 | +claim to fix an issue whose cause it never touched. |
| 38 | + |
| 39 | +## What to look for first |
| 40 | + |
| 41 | +### Silence |
| 42 | + |
| 43 | +The defect this repository has shipped most often is input accepted and then ignored. |
| 44 | + |
| 45 | +- `catch { }` or `catch { return null; }` — swallowing turns a user's mistake into wrong output |
| 46 | +- A parameter parsed into "no value given", after which the cmdlet proceeds with its default |
| 47 | + behaviour. Ignoring a `-Configuration` that could not be read once meant extracting an entire site |
| 48 | + instead of the one list asked for |
| 49 | +- `System.Text.Json` ignores unknown members by default, so a misspelled property silently has no |
| 50 | + effect. Custom enum converters here drop values they cannot parse, case sensitively |
| 51 | +- An unrecognised resource prefix in a permission attribute is silently classified as **SharePoint**, |
| 52 | + so a typo'd `"garph/…"` declares a bogus SharePoint scope while the real Graph requirement goes |
| 53 | + undeclared — see [`permissions-auditor`](../permissions-auditor/SKILL.md) |
| 54 | +- A dropped value that *widens* what the cmdlet does deserves an error, not a warning. An empty |
| 55 | + handler list means "all handlers", so one unrecognised handler name would otherwise turn a scoped |
| 56 | + operation into a full one |
| 57 | + |
| 58 | +### Unpaged collections |
| 59 | + |
| 60 | +`GraphRequestHelper.GetResultCollection` follows `@odata.nextLink`. `GraphRequestHelper.Get` does |
| 61 | +not — pointed at a collection endpoint it returns the **first page only, with no error**. The same |
| 62 | +applies to `RequestHelper`. This presents to users as "the cmdlet misses items in large tenants", |
| 63 | +which is invisible in any tenant small enough to develop against. Check every new collection fetch. |
| 64 | + |
| 65 | +The CSOM equivalent: a query returning more than the list view threshold, or a loop that pages |
| 66 | +manually and drops the last page. |
| 67 | + |
| 68 | +### Base class |
| 69 | + |
| 70 | +Check the base class actually matches what the cmdlet does — a tenant-admin operation on |
| 71 | +`PnPWebCmdlet`, or a Graph call from a SharePoint cmdlet, gets the wrong context and the wrong |
| 72 | +permission flavour. It compiles, and it fails in someone's tenant. The table in |
| 73 | +[`new-cmdlet`](../new-cmdlet/SKILL.md) is the reference. |
| 74 | + |
| 75 | +### How errors reach the user |
| 76 | + |
| 77 | +`PnPConnectedCmdlet.ProcessRecord` rethrows `PipelineStoppedException` untouched |
| 78 | +(`src/Commands/Base/PnPConnectedCmdlet.cs:57-60`) and catches everything else. Two paths, and the |
| 79 | +difference is the finding: |
| 80 | + |
| 81 | +- **`WriteError` / `ThrowTerminatingError`** — under `-ErrorAction Stop` these surface as a pipeline |
| 82 | + stop, rethrown unchanged. The `ErrorRecord`, its `ErrorCategory` and its target object all reach |
| 83 | + the user intact. |
| 84 | +- **A raw `throw`** — hits the generic catch. Default error action: rethrown as |
| 85 | + `PSInvalidOperationException` with the original as inner. Under `-ErrorAction Stop` or |
| 86 | + `SilentlyContinue`: `LogError` → `LoggingUtility.Error` → `WriteError(new ErrorRecord(new |
| 87 | + Exception(message), source, ErrorCategory.NotSpecified, null))`. Type, inner exception, category |
| 88 | + and target object are **all discarded**, so everything the user needs must be in the message text. |
| 89 | + Under `-ErrorAction Ignore` the `LogError` call is skipped altogether |
| 90 | + (`PnPConnectedCmdlet.cs:112-119`), so the failure is **swallowed with no record at all** — worth |
| 91 | + remembering when a user reports a cmdlet that "does nothing and says nothing". |
| 92 | + |
| 93 | +So a raw `throw` carrying a custom exception type the caller is meant to inspect is a finding — the |
| 94 | +type is not observable on that path. So is a fatal condition signalled with `WriteWarning` and then |
| 95 | +continuing, and a `throw` where `ThrowTerminatingError` with a real `ErrorCategory` and target object |
| 96 | +would have told the user which object failed. |
| 97 | + |
| 98 | +### Cmdlet conventions |
| 99 | + |
| 100 | +- `Verb-PnPNoun`, approved verbs, correct base class |
| 101 | +- `ParameterSpecified(nameof(X))` distinguishes "not supplied" from "supplied as default" |
| 102 | +- Reference-typed parameters that are dereferenced in `ExecuteCmdlet` need `[ValidateNotNull]`, |
| 103 | + otherwise `-Param $null` is a `NullReferenceException` |
| 104 | +- Permission attributes must match the APIs the cmdlet actually calls, in both directions — |
| 105 | + over-declaring forces users to grant access the cmdlet never uses |
| 106 | +- Destructive or overwriting behaviour needs `ShouldProcess`, with `-Force` bypassing only a |
| 107 | + secondary `ShouldContinue`. **`Force || ShouldProcess(...)` is a defect**: `-Force` short-circuits |
| 108 | + the `||`, `ShouldProcess` is never called, and `-Force -WhatIf` performs the operation instead of |
| 109 | + simulating it. `Force || ShouldContinue(...)` is the correct, repo-standard form |
| 110 | +- A renamed cmdlet keeps its old name as `[Alias]` |
| 111 | + |
| 112 | +### Cross-platform |
| 113 | + |
| 114 | +.NET 8 and PowerShell 7.4+ on Windows, Linux and macOS. |
| 115 | + |
| 116 | +- No Windows-only path assumptions, no backslash string surgery |
| 117 | +- `Environment.NewLine` (what `StringBuilder.AppendLine` writes) mixed with hardcoded `\r\n` makes |
| 118 | + generated files churn purely from changing OS |
| 119 | +- Format dates and numbers with `CultureInfo.InvariantCulture`. A custom format string like |
| 120 | + `"yyyy-MM-ddTHH:mm:ssZ"` takes its separators from the current culture and produces |
| 121 | + `13.53.41` under some locales, which is not a valid `xsd:dateTime` |
| 122 | +- New package references have ALC consequences: the module assembly and CSOM live in `Core`, |
| 123 | + every other dependency is private and goes to `Common` |
| 124 | + |
| 125 | +## Documentation and changelog |
| 126 | + |
| 127 | +A parameter added, renamed, or changed in behaviour requires its `documentation/<Cmdlet>.md` updated |
| 128 | +in the same PR. A new cmdlet requires a new page; a removed cmdlet requires its page deleted. |
| 129 | + |
| 130 | +- `## PARAMETERS` sections carry only the platyPS ` ```yaml ` metadata blocks. Other fenced blocks |
| 131 | + there risk the help build; put examples under `## EXAMPLES` with ` ```powershell ` fences |
| 132 | +- Parameter subsections are listed alphabetically |
| 133 | +- Conceptual content belongs in `pages/articles/` with front matter, registered in `toc.yml`, not in |
| 134 | + `documentation/`, which is cmdlet reference only |
| 135 | + |
| 136 | +`CHANGELOG.md` entries go under `[Current nightly]` in `Added`, `Changed`, `Fixed` or `Removed`, |
| 137 | +each naming the affected cmdlets in backticks and linking its PR. A change in behaviour belongs |
| 138 | +under `Changed` even when it fixes a bug, so it appears in the release notes people read before |
| 139 | +upgrading. A PR with no changelog entry is an incomplete PR — but the file header says it is owner |
| 140 | +maintained and maintainers do add entries, so raise it as a gap, not a blocker. |
| 141 | + |
| 142 | +## Breaking changes |
| 143 | + |
| 144 | +Ask one question: **does any correct usage behave differently?** |
| 145 | + |
| 146 | +If only previously-broken usage changes — a configuration that was never honoured, an invocation that |
| 147 | +already threw — it is a fix, and it belongs in a minor release with a `Changed` entry. Reserve a major |
| 148 | +release for changes that break usage which was working as documented. Say which of the two a PR is, |
| 149 | +and name the invocation that changes, rather than labelling it breaking on the strength of a diff. |
| 150 | +[`api-surface-diff`](../api-surface-diff/SKILL.md) has the full classification. |
| 151 | + |
| 152 | +## Reporting |
| 153 | + |
| 154 | +Lead with the finding, not the file tour. For each one give the location, one sentence on the defect, |
| 155 | +and a concrete failure scenario: the input, and what the user gets instead of what they expected. |
| 156 | +Rank by consequence. If a check could not be run, say what would settle it. |
0 commit comments