diff --git a/Rules/CompatibilityRules/UseCompatibleSyntax.cs b/Rules/CompatibilityRules/UseCompatibleSyntax.cs index 2863d1718..3af6cdd1c 100644 --- a/Rules/CompatibilityRules/UseCompatibleSyntax.cs +++ b/Rules/CompatibilityRules/UseCompatibleSyntax.cs @@ -183,6 +183,22 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio { // Look for [typename]::new(...) and [typename]::$dynamicMethodName syntax +#if PSV7 + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + if (methodCallAst.NullConditional) + { + AddDiagnostic( + methodCallAst, + "null-conditional method invocation", + "${x}?.Method()", + "3,4,5,6"); + } +#endif + if (!_targetVersions.Contains(s_v3) && !_targetVersions.Contains(s_v4)) { return AstVisitAction.Continue; @@ -190,20 +206,11 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio if (_targetVersions.Contains(s_v3) && methodCallAst.Member is VariableExpressionAst) { - string message = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCompatibleSyntaxError, + AddDiagnostic( + methodCallAst, "dynamic method invocation", - methodCallAst.Extent.Text, + "$x.$method()", "3"); - - _diagnosticAccumulator.Add(new DiagnosticRecord( - message, - methodCallAst.Extent, - _rule.GetName(), - _rule.Severity, - _analyzedFilePath - )); } if (!(methodCallAst.Expression is TypeExpressionAst typeExpressionAst)) @@ -226,22 +233,7 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio typeName, methodCallAst.Arguments); - string message = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCompatibleSyntaxError, - "constructor", - methodCallAst.Extent.Text, - "3,4"); - - _diagnosticAccumulator.Add(new DiagnosticRecord( - message, - methodCallAst.Extent, - _rule.GetName(), - _rule.Severity, - _analyzedFilePath, - ruleId: null, - suggestedCorrections: new [] { suggestedCorrection } - )); + AddDiagnostic(methodCallAst, "constructor", "[type]::new()", "3,4", suggestedCorrection); return AstVisitAction.Continue; } @@ -263,21 +255,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun return AstVisitAction.Continue; } - string message = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCompatibleSyntaxError, - "workflow", - "workflow { ... }", - "6"); - - _diagnosticAccumulator.Add( - new DiagnosticRecord( - message, - functionDefinitionAst.Extent, - _rule.GetName(), - _rule.Severity, - _analyzedFilePath - )); + AddDiagnostic(functionDefinitionAst, "workflow", "workflow { ... }", "6,7"); return AstVisitAction.Continue; } @@ -292,22 +270,12 @@ public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatem return AstVisitAction.Continue; } - string message = string.Format( - CultureInfo.CurrentCulture, - Strings.UseCompatibleSyntaxError, + AddDiagnostic( + usingStatementAst, "using statement", "using ...;", "3,4"); - _diagnosticAccumulator.Add( - new DiagnosticRecord( - message, - usingStatementAst.Extent, - _rule.GetName(), - _rule.Severity, - _analyzedFilePath - )); - return AstVisitAction.Continue; } @@ -340,6 +308,145 @@ public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinit } #endif +#if PSV7 + public override AstVisitAction VisitMemberExpression(MemberExpressionAst memberExpressionAst) + { + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + if (memberExpressionAst.NullConditional) + { + AddDiagnostic( + memberExpressionAst, + "null-conditional member access", + "${x}?.Member", + "3,4,5,6"); + } + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + if (assignmentStatementAst.Operator == TokenKind.QuestionQuestionEquals) + { + AddDiagnostic(assignmentStatementAst, "null-conditional assignment", "$x ??= $y", "3,4,5,6"); + } + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) + { + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + if (binaryExpressionAst.Operator == TokenKind.QuestionQuestion) + { + AddDiagnostic( + binaryExpressionAst, + "null-coalescing operator", + "$x ?? $y", + "3,4,5,6"); + } + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) + { + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + var correction = new CorrectionExtent( + ternaryExpressionAst.Extent, + $"if ({ternaryExpressionAst.Condition.Extent.Text}) {{ {ternaryExpressionAst.IfTrue.Extent.Text} }} else {{ {ternaryExpressionAst.IfFalse.Extent.Text} }}", + _analyzedFilePath); + + AddDiagnostic( + ternaryExpressionAst, + "ternary expression", + " ? : ", + "3,4,5,6", + correction); + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitPipelineChain(PipelineChainAst statementChain) + { + if (!TargetsNonPS7()) + { + return AstVisitAction.Continue; + } + + AddDiagnostic( + statementChain, + "pipeline chain", + " && OR || ", + "3,4,5,6"); + + return AstVisitAction.Continue; + } + + private bool TargetsNonPS7() + { + return _targetVersions.Contains(s_v3) + || _targetVersions.Contains(s_v4) + || _targetVersions.Contains(s_v5) + || _targetVersions.Contains(s_v6); + } +#endif + + private void AddDiagnostic( + Ast offendingAst, + string syntaxName, + string syntaxExample, + string unsupportedVersions, + CorrectionExtent correction = null) + { + string message = string.Format( + CultureInfo.CurrentCulture, + Strings.UseCompatibleSyntaxError, + syntaxName, + syntaxExample, + unsupportedVersions); + + if (correction == null) + { + _diagnosticAccumulator.Add( + new DiagnosticRecord( + message, + offendingAst.Extent, + _rule.GetName(), + _rule.Severity, + _analyzedFilePath)); + + return; + } + + _diagnosticAccumulator.Add( + new DiagnosticRecord( + message, + offendingAst.Extent, + _rule.GetName(), + _rule.Severity, + _analyzedFilePath, + ruleId: null, + new[] { correction })); + } + private static CorrectionExtent CreateNewObjectCorrection( string filePath, IScriptExtent offendingExtent, diff --git a/Tests/Rules/UseCompatibleSyntax.Tests.ps1 b/Tests/Rules/UseCompatibleSyntax.Tests.ps1 index dd6497fb3..150ef2ecf 100644 --- a/Tests/Rules/UseCompatibleSyntax.Tests.ps1 +++ b/Tests/Rules/UseCompatibleSyntax.Tests.ps1 @@ -31,6 +31,23 @@ Describe "PSUseCompatibleSyntax" { @{ Script = 'workflow Banana { Do-ExpensiveCommandOnAnotherMachine -Argument "Banana" }'; Versions = @(6) } ) } + + if ($PSVersionTable.PSVersion.Major -ge 7) + { + $testCases += @( + @{ Script = '$x = $path ? (Get-Content -Raw $path) : "default"'; Versions = @(3,4,5,6) } + @{ Script = '$x ??= 7'; Versions = @(3,4,5,6) } + @{ Script = 'git pull origin master && git pull upstream master'; Versions = @(3,4,5,6) } + ) + + if ((Get-ExperimentalFeature -Name 'PSNullConditionalOperators').Enabled) + { + $testCases += @( + @{ Script = '${item}?.Invoke()'; Versions = @(3,4,5,6) } + @{ Script = '${object}?.Member'; Versions = @(3,4,5,6) } + ) + } + } } foreach ($v in 3,4,5,6)