Skip to main content

Quality Gates & Code Standards

This page documents the quality gates and code standards enforced in the NinjaOne module through PSScriptAnalyzer and custom rules.

Overview

Code quality is maintained through:

  • PSScriptAnalyzer - Static analysis rules from the PSScriptAnalyzer module
  • Custom Rules - Project-specific rules that enforce module conventions
  • Parameter Documentation - Inline comment descriptions emitted as the PSMissingParameterInlineComment diagnostic
  • Help Documentation - Comment-based help for all public functions

Running Quality Checks

Full Analysis

Run all quality checks including tests and analysis:

pwsh -File .\DevOps\Quality\test.ps1

Analysis Only

Run PSScriptAnalyzer without tests:

pwsh -File .\DevOps\Quality\run-pssa.ps1

The analyzer uses PSScriptAnalyzerSettings.psd1 and excludes:

  • CustomRules - Custom rule definitions themselves
  • output - Generated build artifacts
  • Modules - Third-party bundled modules
  • test-rules.ps1 - Test helper for PSSA itself

The repo lint entrypoints also split rule scope deliberately:

  • Full settings are applied to Source\Public
  • Non-public files are analyzed with a reduced settings object

This is how the repo currently enforces public-surface conventions such as camelCase parameter names without forcing the same rule across every test and helper script.

Custom Rules

Custom rules are defined in CustomRules/ and loaded automatically by the test and analysis scripts.

Common custom rules currently enforced by the repo include:

Diagnostic IDImplemented ByScope in repo lintingPurpose
PSMissingParameterInlineCommentMeasure-MissingParameterDescriptionscanned files unless suppressedRequires a comment line immediately before each parameter
PSRequiredCommentBasedHelpMeasure-RequiredCommentBasedHelpscanned filesRequires comment-based help; public functions also need .DESCRIPTION and .EXAMPLE
PSCommentBasedHelpEmptySectionMeasure-EmptyCommentBasedHelpSectionsscanned filesFlags empty CBH sections
PSUseCamelCaseParameterNameMeasure-RequireCamelCaseParameterNameSource\Public in repo lint entrypointsEnforces camelCase parameter names on exported cmdlets
PSAvoidSelfReferentialParameterAliasMeasure-AvoidSelfReferentialParameterAliasSource\PublicFlags aliases that duplicate the parameter name
PSUseProperTypeAcceleratorCasingMeasure-RequireProperTypeAcceleratorCasingSource\PublicEnforces canonical casing for type accelerators

Measure-MissingParameterDescription (PSMissingParameterInlineComment)

Rule: Parameters in analyzed functions must have a comment line immediately before the parameter declaration unless that diagnostic is suppressed.

Purpose: Ensures parameters are described close to the declaration site and keeps generated help grounded in the source.

Example - Bad:

function Get-NinjaOneDevice {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$deviceId,
[string]$filter
)

Example - Good:

function Get-NinjaOneDevice {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
# The device ID to retrieve information for.
[string]$deviceId,
# Optional filter expression. See the filters documentation for more information.
[string]$filter
)

For Public Functions:

  • Required in normal repo linting.
  • These comments should sit immediately before the parameter, not after it.

For Internal Functions:

  • The diagnostic still exists when those files are analyzed.
  • Internal/dev/test scripts often suppress it explicitly when full parameter prose would add noise.

Suppressing Rules

Some internal scripts (DevOps tooling, private functions, test utilities) don't require full parameter documentation. Use diagnostic suppression attributes to explicitly allow this.

Suppression Syntax

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('RuleName', '', Justification = 'reason')]

Script-Level Suppression

Apply at the top of the script file to suppress the rule for all functions:

#requires -Version 7
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSMissingParameterInlineComment', '', Justification = 'Internal DevOps script does not require parameter descriptions.')]
param()

function Build-Module {
param(
[string]$config,
[switch]$verbose
)
# ...
}

Function-Level Suppression

Apply immediately before the function declaration to suppress only that function:

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSMissingParameterInlineComment', '', Justification = 'Private helper function used internally.')]
function ConvertTo-JsonString {
param(
[hashtable]$data,
[int]$depth
)
# ...
}

Nested Function Suppression

Each nested function requires its own suppression:

function Test-Configuration {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSMissingParameterInlineComment', '', Justification = 'Nested test helper.')]
function ValidateInput {
param([string]$value)
return $value -ne $null
}

# Main logic
}

Built-In Rules

Common PSScriptAnalyzer rules enforced in this project:

RuleSeverityPurpose
PSUseSingularNounsWarningEncourages singular naming for non-collection objects
PSAvoidDefaultValueSwitchParameterWarningPrevents switch parameters with default values
PSUseCompatibleSyntaxErrorEnsures PowerShell 7+ compatibility
PSAvoidUsingPositionalParametersWarningEncourages named parameters in function calls
PSUseOutputTypeCorrectlyWarningValidates [OutputType()] attributes
PSAvoidUsingInvokeExpressionErrorPrevents security vulnerabilities with dynamic code
PSAvoidUsingConvertToSecureStringWithPlainTextErrorPrevents plaintext password exposure

For the full list, see PSScriptAnalyzerSettings.psd1.

Parameter Documentation Examples

Minimal Good Parameter

param(
[Parameter(Mandatory)]
# Device identifier
[string]$deviceId
)

Comprehensive Parameter

param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[Alias('DeviceID', 'ID')]
# Device object(s) to retrieve information for
[object[]]$device,

[Parameter(ValueFromPipelineByPropertyName)]
[ValidateSet('Active', 'Inactive', 'Archived')]
# Filter by device status; defaults to Active
[string]$status = 'Active',

# Include detailed device information
[switch]$includeDetails,

# Results per page; defaults to 100
[int]$pageSize = 100
)

Key points:

  • First line: Brief purpose or value description
  • For switches: Explain what enabling the switch does
  • For parameters with defaults: Explain the default behavior
  • For enums: List valid options if not self-explanatory
  • For collections: Clarify if single or multiple values accepted

Documentation Quality Checklist

Before submitting a PR with new or modified public functions:

  • All parameters have inline descriptions (comment line immediately before the parameter)
  • Descriptions follow the style guide above
  • Comment-based help is present and accurate
  • Any hand-written .PARAMETER sections are still accurate if present
  • PSScriptAnalyzer passes without warnings
  • No placeholder text in generated docs (e.g., Fill parameter Description)

Troubleshooting Quality Failures

"PSScriptAnalyzer found violations"

Run the analyzer to see details:

pwsh -File .\DevOps\Quality\run-pssa.ps1

"Directory scan shows 0 violations but file scan shows many"

When running Invoke-ScriptAnalyzer with a directory path, you must use the -Recurse parameter to scan subdirectories:

# ❌ This won't find violations in subdirectories
Invoke-ScriptAnalyzer -Path .\Source

# ✅ This will find all violations
Invoke-ScriptAnalyzer -Path .\Source -Recurse

The run-pssa.ps1 script scans files recursively and applies the public-only rules through a split-settings approach.

"Rule doesn't apply to my function"

Verify the rule scope:

  • PSMissingParameterInlineComment can appear anywhere the analyzer scans unless it is suppressed
  • PSUseCamelCaseParameterName, PSAvoidSelfReferentialParameterAlias, and PSUseProperTypeAcceleratorCasing are intentionally enforced on Source\Public in the repo lint entrypoints
  • If suppressing, use a clear justification explaining why the diagnostic should not apply

"Suppression attribute isn't working"

Common issues:

  1. Wrong rule name: Must match exactly (e.g., PSMissingParameterInlineComment, not PSMissingParameter)

  2. Wrong indentation: Ensure the attribute is immediately before the function:

    # ❌ Won't work - extra line
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(...)]

    function MyFunction {

    # ✅ Works - directly before function
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(...)]
    function MyFunction {
  3. Nested function scope: Nested functions need individual suppressions; script-level suppressions don't cascade

See Also