Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions Rules/MissingTryBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Management.Automation.Language;

#if !CORECLR
using System.ComponentModel.Composition;
#endif

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif

/// <summary>
/// Rule that warns when reserved words are used as function names
/// </summary>
public class MissingTryBlock : IScriptRule
{
/// <summary>
/// Analyzes the PowerShell AST for uses of reserved words as function names.
/// </summary>
/// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param>
/// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param>
/// <returns>A collection of diagnostic records for each violation.</returns>
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);

// Find all FunctionDefinitionAst in the Ast
var missingTryAsts = ast.FindAll(testAst =>
// Normally should be part of a TryStatementAst
testAst is StringConstantExpressionAst stringAst &&
// Catch of finally are reserved keywords and should be bare words
stringAst.StringConstantType == StringConstantType.BareWord &&
(
String.Equals(stringAst.Value, "catch", StringComparison.OrdinalIgnoreCase) ||
String.Equals(stringAst.Value, "finally", StringComparison.OrdinalIgnoreCase)
) &&
stringAst.Parent is CommandAst commandAst &&
// Only violate if the catch or finally is the first command element
commandAst.CommandElements[0] == stringAst,
true
);

foreach (StringConstantExpressionAst missingTryAst in missingTryAsts)
{
yield return new DiagnosticRecord(
string.Format(
CultureInfo.CurrentCulture,
Strings.MissingTryBlockError,
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(missingTryAst.Value)),
missingTryAst.Extent,
GetName(),
DiagnosticSeverity.Error,
fileName,
missingTryAst.Value
);
}
}

public string GetCommonName() => Strings.MissingTryBlockCommonName;

public string GetDescription() => Strings.MissingTryBlockDescription;

public string GetName() => string.Format(
CultureInfo.CurrentCulture,
Strings.NameSpaceFormat,
GetSourceName(),
Strings.MissingTryBlockName);

public RuleSeverity GetSeverity() => RuleSeverity.Warning;

public string GetSourceName() => Strings.SourceName;

public SourceType GetSourceType() => SourceType.Builtin;
}
}
12 changes: 12 additions & 0 deletions Rules/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,18 @@
<data name="MissingModuleManifestFieldCommonName" xml:space="preserve">
<value>Module Manifest Fields</value>
</data>
<data name="MissingTryBlockName" xml:space="preserve">
<value>MissingTryBlock</value>
</data>
<data name="MissingTryBlockCommonName" xml:space="preserve">
<value>Missing try block</value>
</data>
<data name="MissingTryBlockDescription" xml:space="preserve">
<value>The catch and finally blocks should be preceded by a try block.</value>
</data>
<data name="MissingTryBlockError" xml:space="preserve">
<value>{0} is missing a try block</value>
</data>
<data name="AvoidUnloadableModuleDescription" xml:space="preserve">
<value>If a script file is in a PowerShell module folder, then that folder must be loadable.</value>
</data>
Expand Down
133 changes: 133 additions & 0 deletions Tests/Rules/MissingTryBlock.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'False positive')]
param()

BeforeAll {
$ruleName = "PSMissingTryBlock"
}

Describe "MissingTryBlock" {
Context "Violates" {
It "Catch is missing a try block" {
$scriptDefinition = { catch { "An error occurred." } }.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 1
$violations.Severity | Should -Be Error
$violations.Extent.Text | Should -Be catch
$violations.Message | Should -Be 'Catch is missing a try block'
$violations.RuleSuppressionID | Should -Be catch
}

It "Finally is missing a try block" {
$scriptDefinition = { finally { "Finalizing..." } }.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 1
$violations.Severity | Should -Be Error
$violations.Extent.Text | Should -Be finally
$violations.Message | Should -Be 'Finally is missing a try block'
$violations.RuleSuppressionID | Should -Be finally
}

It "Single line catch and finally is missing a try block" {
$scriptDefinition = {
catch { "An error occurred." } finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 1
$violations.Severity | Should -Be Error
$violations.Extent.Text | Should -Be catch
$violations.Message | Should -Be 'Catch is missing a try block'
$violations.RuleSuppressionID | Should -Be catch
}

It "Multi line catch and finally is missing a try block" {
$scriptDefinition = {
catch { "An error occurred." }
finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 2
$violations[0].Severity | Should -Be Error
$violations[0].Extent.Text | Should -Be catch
$violations[0].Message | Should -Be 'Catch is missing a try block'
$violations[0].RuleSuppressionID | Should -Be catch
$violations[1].Severity | Should -Be Error
$violations[1].Extent.Text | Should -Be finally
$violations[1].Message | Should -Be 'Finally is missing a try block'
$violations[1].RuleSuppressionID | Should -Be finally
}
}

Context "Compliant" {
It "try-catch block" {
$scriptDefinition = {
try { NonsenseString }
catch { "An error occurred." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "try-catch-final statement" {
$scriptDefinition = {
try { NonsenseString }
catch { "An error occurred." }
finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "Single line try statement" {
$scriptDefinition = {
try { NonsenseString } catch { "An error occurred." } finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "Catch as parameter" {
$scriptDefinition = { Write-Host Catch }.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "Catch as double quoted string" {
$scriptDefinition = { "Catch" }.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "Catch as single quoted string" {
$scriptDefinition = { 'Catch' }.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}
}

Context "Suppressed" {
It "Multi line catch and finally is missing a try block" {
$scriptDefinition = {
[Diagnostics.CodeAnalysis.SuppressMessage('PSMissingTryBlock', '', Justification = 'Test')]
param()
catch { "An error occurred." }
finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations | Should -BeNullOrEmpty
}

It "Multi line catch and finally is missing a try block for catch only" {
$scriptDefinition = {
[Diagnostics.CodeAnalysis.SuppressMessage('PSMissingTryBlock', 'finally', Justification = 'Test')]
param()
catch { "An error occurred." }
finally { "Finalizing..." }
}.ToString()
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule @($ruleName)
$violations.Count | Should -Be 1
}
}
}
34 changes: 34 additions & 0 deletions docs/Rules/MissingTryBlock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
description: Missing Try Block
ms.date: 04/22/2026
ms.topic: reference
title: MissingTryBlock
---
# MissingTryBlock

**Severity Level: Error**

## Description

The `catch` and `finally` blocks should be preceded by a `try` block.
Otherwise, the `catch` and `finally` blocks will be interpreted as commands, which is likely a mistake and result
in a "*The term 'catch' is not recognized as a name of a cmdlet*" error at runtime.

## How

Add a `try` block before the `catch` and `finally` blocks.

## Example

### Wrong

```powershell
catch { "An error occurred." }
```

### Correct

```powershell
try { $a = 1 / $b }
catch { "Attempted to divide by zero." }
```
1 change: 1 addition & 0 deletions docs/Rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The PSScriptAnalyzer contains the following rule definitions.
| [DSCUseVerboseMessageInDSCResource](./DSCUseVerboseMessageInDSCResource.md) | Error | Yes | |
| [MisleadingBacktick](./MisleadingBacktick.md) | Warning | Yes | |
| [MissingModuleManifestField](./MissingModuleManifestField.md) | Warning | Yes | |
| [MissingTryBlock](./MissingTryBlock.md) | Error | Yes | |
| [PlaceCloseBrace](./PlaceCloseBrace.md) | Warning | No | Yes |
| [PlaceOpenBrace](./PlaceOpenBrace.md) | Warning | No | Yes |
| [PossibleIncorrectComparisonWithNull](./PossibleIncorrectComparisonWithNull.md) | Warning | Yes | |
Expand Down