// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Management.Automation.Language;
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
using System.Management.Automation;
using System.Linq;
#if !CORECLR
using System.ComponentModel.Composition;
#endif
using System.Globalization;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
///
/// UseCorrectCasing: Check if cmdlet is cased correctly.
///
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
public class UseCorrectCasing : ConfigurableRule
{
///
/// AnalyzeScript: Analyze the script to check if cmdlet alias is used.
///
public override IEnumerable AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
IEnumerable commandAsts = ast.FindAll(testAst => testAst is CommandAst, true);
// Iterates all CommandAsts and check the command name.
foreach (CommandAst commandAst in commandAsts)
{
string commandName = commandAst.GetCommandName();
// Handles the exception caused by commands like, {& $PLINK $args 2> $TempErrorFile}.
// You can also review the remark section in following document,
// MSDN: CommandAst.GetCommandName Method
if (commandName == null)
{
continue;
}
var commandInfo = Helper.Instance.GetCommandInfo(commandName);
if (commandInfo == null || commandInfo.CommandType == CommandTypes.ExternalScript || commandInfo.CommandType == CommandTypes.Application)
{
continue;
}
var shortName = commandInfo.Name;
var fullyqualifiedName = $"{commandInfo.ModuleName}\\{shortName}";
var isFullyQualified = commandName.Equals(fullyqualifiedName, StringComparison.OrdinalIgnoreCase);
var correctlyCasedCommandName = isFullyQualified ? fullyqualifiedName : shortName;
if (!commandName.Equals(correctlyCasedCommandName, StringComparison.Ordinal))
{
yield return new DiagnosticRecord(
string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingError, commandName, correctlyCasedCommandName),
GetCommandExtent(commandAst),
GetName(),
DiagnosticSeverity.Warning,
fileName,
commandName,
suggestedCorrections: GetCorrectionExtent(commandAst, correctlyCasedCommandName));
}
var commandParameterAsts = commandAst.FindAll(
testAst => testAst is CommandParameterAst, true).Cast();
Dictionary availableParameters;
try
{
availableParameters = commandInfo.Parameters;
}
// It's a known issue that objects from PowerShell can have a runspace affinity,
// therefore if that happens, we query a fresh object instead of using the cache.
// https://github.com/PowerShell/PowerShell/issues/4003
catch (InvalidOperationException)
{
commandInfo = Helper.Instance.GetCommandInfo(commandName, bypassCache: true);
availableParameters = commandInfo.Parameters;
}
foreach (var commandParameterAst in commandParameterAsts)
{
var parameterName = commandParameterAst.ParameterName;
if (availableParameters.TryGetValue(parameterName, out ParameterMetadata parameterMetaData))
{
var correctlyCasedParameterName = parameterMetaData.Name;
if (!parameterName.Equals(correctlyCasedParameterName, StringComparison.Ordinal))
{
yield return new DiagnosticRecord(
string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingParameterError, parameterName, commandName, correctlyCasedParameterName),
GetCommandExtent(commandAst),
GetName(),
DiagnosticSeverity.Warning,
fileName,
commandName,
suggestedCorrections: GetCorrectionExtent(commandParameterAst, correctlyCasedParameterName));
}
}
}
}
}
///
/// For a command like "gci -path c:", returns the extent of "gci" in the command
///
private IScriptExtent GetCommandExtent(CommandAst commandAst)
{
var cmdName = commandAst.GetCommandName();
foreach (var cmdElement in commandAst.CommandElements)
{
var stringConstExpressinAst = cmdElement as StringConstantExpressionAst;
if (stringConstExpressinAst != null)
{
if (stringConstExpressinAst.Value.Equals(cmdName))
{
return stringConstExpressinAst.Extent;
}
}
}
return commandAst.Extent;
}
private IEnumerable GetCorrectionExtent(CommandAst commandAst, string correctlyCaseName)
{
var description = string.Format(
CultureInfo.CurrentCulture,
Strings.UseCorrectCasingDescription,
correctlyCaseName,
correctlyCaseName);
var cmdExtent = GetCommandExtent(commandAst);
var correction = new CorrectionExtent(
cmdExtent.StartLineNumber,
cmdExtent.EndLineNumber,
cmdExtent.StartColumnNumber,
cmdExtent.EndColumnNumber,
correctlyCaseName,
commandAst.Extent.File,
description);
yield return correction;
}
private IEnumerable GetCorrectionExtent(CommandParameterAst commandParameterAst, string correctlyCaseName)
{
var description = string.Format(
CultureInfo.CurrentCulture,
Strings.UseCorrectCasingDescription,
correctlyCaseName,
correctlyCaseName);
var cmdExtent = commandParameterAst.Extent;
var correction = new CorrectionExtent(
cmdExtent.StartLineNumber,
cmdExtent.EndLineNumber,
// +1 because of the dash before the parameter name
cmdExtent.StartColumnNumber + 1,
// do not use EndColumnNumber property as it would not cover the case where the colon syntax: -ParameterName:$ParameterValue
cmdExtent.StartColumnNumber + 1 + commandParameterAst.ParameterName.Length,
correctlyCaseName,
commandParameterAst.Extent.File,
description);
yield return correction;
}
///
/// GetName: Retrieves the name of this rule.
///
/// The name of this rule
public override string GetName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseCorrectCasingName);
}
///
/// GetCommonName: Retrieves the common name of this rule.
///
/// The common name of this rule
public override string GetCommonName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingCommonName);
}
///
/// GetDescription: Retrieves the description of this rule.
///
/// The description of this rule
public override string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingDescription);
}
///
/// GetSourceType: Retrieves the type of the rule, Builtin, Managed or Module.
///
public override SourceType GetSourceType()
{
return SourceType.Builtin;
}
///
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
///
///
public override RuleSeverity GetSeverity()
{
return RuleSeverity.Information;
}
///
/// GetSourceName: Retrieves the name of the module/assembly the rule is from.
///
public override string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}
}
}