diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs index 55aa4296e..1375d1f10 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs @@ -20,7 +20,6 @@ using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Analysis.Types; -using Microsoft.Python.Analysis.Utilities; using Microsoft.Python.Analysis.Values; using Microsoft.Python.Core; using Microsoft.Python.Core.Disposables; @@ -108,7 +107,7 @@ public Location GetLocationOfName(Node node) { public void ReportDiagnostics(Uri documentUri, DiagnosticsEntry entry) { // Do not add if module is library, etc. Only handle user code. - if (Module.ModuleType == ModuleType.User) { + if (entry.ShouldReport(Module)) { lock (_lock) { _diagnostics.Add(entry); } diff --git a/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs b/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs index 0f3c037fc..66165504b 100644 --- a/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs +++ b/src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs @@ -174,7 +174,7 @@ public IReadOnlyList LintModule(IPythonModule module) { // Linter always runs no matter of the option since it looks up variables // which also enumerates and updates variable references for find all // references and rename operations. - var result = new LinterAggregator().Lint(module.Analysis, _services); + var result = new LinterAggregator().Lint(module, _services); var optionsProvider = _services.GetService(); return optionsProvider?.Options?.LintingEnabled == false ? Array.Empty() : result; diff --git a/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsEntry.cs b/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsEntry.cs index 25b174e69..f9e70f117 100644 --- a/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsEntry.cs +++ b/src/Analysis/Ast/Impl/Diagnostics/DiagnosticsEntry.cs @@ -13,6 +13,9 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using Microsoft.Python.Analysis.Modules; +using Microsoft.Python.Analysis.Types; +using Microsoft.Python.Core; using Microsoft.Python.Core.Text; using Microsoft.Python.Parsing; @@ -51,6 +54,20 @@ public DiagnosticsEntry(string message, SourceSpan span, string errorCode, Sever /// public DiagnosticSource Source { get; } + public bool ShouldReport(IPythonModule module) { + // Only report for user written modules + if (module.ModuleType != ModuleType.User) { + return false; + } + + // If user specifies #noqa, then do not report diagnostic + if (module.GetComment(SourceSpan.Start.Line).EqualsIgnoreCase("noqa")) { + return false; + } + + return true; + } + public override bool Equals(object obj) { if (!(obj is DiagnosticsEntry e)) { return false; diff --git a/src/Analysis/Ast/Impl/Extensions/AstExtensions.cs b/src/Analysis/Ast/Impl/Extensions/AstExtensions.cs index a1d2b5d8f..65234d56d 100644 --- a/src/Analysis/Ast/Impl/Extensions/AstExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/AstExtensions.cs @@ -17,16 +17,18 @@ using System.Diagnostics; using System.Linq; using Microsoft.Python.Analysis.Analyzer.Expressions; +using Microsoft.Python.Analysis.Modules; +using Microsoft.Python.Analysis.Types; using Microsoft.Python.Core.Text; using Microsoft.Python.Parsing; using Microsoft.Python.Parsing.Ast; namespace Microsoft.Python.Analysis { public static class AstExtensions { - public static Expression FindExpression(this PythonAst ast, int index, FindExpressionOptions options) + public static Expression FindExpression(this PythonAst ast, int index, FindExpressionOptions options) => new ExpressionFinder(ast, options).GetExpression(index) as Expression; - public static Expression FindExpression(this PythonAst ast, SourceLocation location, FindExpressionOptions options) + public static Expression FindExpression(this PythonAst ast, SourceLocation location, FindExpressionOptions options) => new ExpressionFinder(ast, options).GetExpression(location) as Expression; public static string GetDocumentation(this ScopeStatement node) { @@ -62,7 +64,7 @@ public static bool IsInsideComment(this PythonAst ast, SourceLocation location) public static bool IsInsideString(this PythonAst ast, SourceLocation location) { var index = ast.LocationToIndex(location); - return ast.FindExpression(index, new FindExpressionOptions {Literals = true}) != null; + return ast.FindExpression(index, new FindExpressionOptions { Literals = true }) != null; } public static bool IsInParameter(this FunctionDefinition fd, PythonAst tree, SourceLocation location) { diff --git a/src/Analysis/Ast/Impl/Extensions/PythonModuleExtensions.cs b/src/Analysis/Ast/Impl/Extensions/PythonModuleExtensions.cs index c33b4620f..0cdbcb0f4 100644 --- a/src/Analysis/Ast/Impl/Extensions/PythonModuleExtensions.cs +++ b/src/Analysis/Ast/Impl/Extensions/PythonModuleExtensions.cs @@ -15,6 +15,7 @@ using Microsoft.Python.Analysis.Modules; using Microsoft.Python.Analysis.Types; +using Microsoft.Python.Core.Text; using Microsoft.Python.Parsing.Ast; namespace Microsoft.Python.Analysis { @@ -26,5 +27,39 @@ internal static T GetAstNode(this IPythonModule module, object o) where T : N => ((IAstNodeContainer)module).GetAstNode(o); internal static void AddAstNode(this IPythonModule module, object o, Node n) => ((IAstNodeContainer)module).AddAstNode(o, n); + + /// + /// Returns the string line corresponding to the given line number + /// + /// The line number + internal static string GetLine(this IPythonModule module, int lineNum) { + string content = module.Analysis?.Document?.Content; + if (string.IsNullOrEmpty(content)) { + return string.Empty; + } + + SourceLocation source = new SourceLocation(lineNum, 1); + var start = module.GetAst().LocationToIndex(source); + var end = start; + + + for (; end < content.Length && content[end] != '\n' && content[end] != '\r'; end++) ; + return content.Substring(start, end - start).Trim('\t', ' '); + } + + /// + /// Returns the comment corresponding to the given line or an empty string if there is no comment + /// + /// The line number + internal static string GetComment(this IPythonModule module, int lineNum) { + string line = module.GetLine(lineNum); + + int commentPos = line.IndexOf('#'); + if (commentPos < 0) { + return string.Empty; + } + + return line.Substring(commentPos + 1).Trim('\t', ' '); + } } } diff --git a/src/Analysis/Ast/Impl/Linting/LinterAggregator.cs b/src/Analysis/Ast/Impl/Linting/LinterAggregator.cs index 948bd55f3..915ddf4a5 100644 --- a/src/Analysis/Ast/Impl/Linting/LinterAggregator.cs +++ b/src/Analysis/Ast/Impl/Linting/LinterAggregator.cs @@ -17,6 +17,7 @@ using System.Linq; using Microsoft.Python.Analysis.Diagnostics; using Microsoft.Python.Analysis.Linting.UndefinedVariables; +using Microsoft.Python.Analysis.Types; using Microsoft.Python.Core; namespace Microsoft.Python.Analysis.Linting { @@ -27,7 +28,7 @@ public LinterAggregator() { // TODO: develop mechanism for dynamic and external linter discovery. _linters.Add(new UndefinedVariablesLinter()); } - public IReadOnlyList Lint(IDocumentAnalysis analysis, IServiceContainer services) - => _linters.SelectMany(l => l.Lint(analysis, services)).ToArray(); + public IReadOnlyList Lint(IPythonModule module, IServiceContainer services) + => _linters.SelectMany(l => l.Lint(module.Analysis, services)).Where(d => d.ShouldReport(module)).ToArray(); } } diff --git a/src/Analysis/Ast/Impl/Modules/PythonModule.cs b/src/Analysis/Ast/Impl/Modules/PythonModule.cs index 81664250d..25c507bd0 100644 --- a/src/Analysis/Ast/Impl/Modules/PythonModule.cs +++ b/src/Analysis/Ast/Impl/Modules/PythonModule.cs @@ -241,8 +241,7 @@ public string Content { return _buffer.Text; } } - } - + } #endregion #region Parsing diff --git a/src/Analysis/Ast/Test/LintNoQATests.cs b/src/Analysis/Ast/Test/LintNoQATests.cs new file mode 100644 index 000000000..92c1bf924 --- /dev/null +++ b/src/Analysis/Ast/Test/LintNoQATests.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Python.Analysis.Analyzer; +using Microsoft.Python.Analysis.Core.Interpreter; +using Microsoft.Python.Analysis.Diagnostics; +using Microsoft.Python.Analysis.Tests.FluentAssertions; +using Microsoft.Python.Core; +using Microsoft.Python.Parsing; +using Microsoft.Python.Parsing.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestUtilities; +using ErrorCodes = Microsoft.Python.Analysis.Diagnostics.ErrorCodes; + +namespace Microsoft.Python.Analysis.Tests { + + [TestClass] + public class LintNoQATests : AnalysisTestBase { + + public const string GenericSetup = @" +from typing import Generic, TypeVar +T = TypeVar('T', int, str) +T1 = TypeVar('T1', int, str) + +_X = TypeVar('_X', str, int) +_T = _X +"; + + public TestContext TestContext { get; set; } + + [TestInitialize] + public void TestInitialize() + => TestEnvironmentImpl.TestInitialize($"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}"); + + [TestCleanup] + public void Cleanup() => TestEnvironmentImpl.TestCleanup(); + + [DataRow("x = Generic[T, str] #noqa")] + [DataRow("x = Generic[T, T] #noqa ")] + [DataTestMethod, Priority(0)] + public async Task IgnoreGenerics(string decl) { + string code = GenericSetup + decl; + + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [DataRow("x = 'str' + 5 #noqa")] + [DataRow("x = float(1) * 'str' #noqa")] + [DataTestMethod, Priority(0)] + public async Task IgnoreBadBinaryOp(string code) { + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [DataRow("x = 'str' + 5 # noqa")] + [DataRow("x = float(1) * 'str' # noqa")] + [DataTestMethod, Priority(0)] + public async Task IgnoreNoQAWithSpace(string code) { + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [DataRow("x = 'str' + 5 # noqa")] + [DataRow("x = float(1) * 'str' # noqa")] + [DataTestMethod, Priority(0)] + public async Task IgnoreNoQAWithTab(string code) { + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [DataRow("x = 'str' + 5 #NOQA")] + [DataRow("x = float(1) * 'str' # NOQA")] + [DataRow("x = float(1) * 'str' # NOQA")] + [DataTestMethod, Priority(0)] + public async Task IgnoreNoQAUppercase(string code) { + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task VarNamedNoQAStillGivesDiagnostic() { + const string code = @" +noqa = 1 + 'str' +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().HaveCount(1); + + var diagnostic = analysis.Diagnostics.ElementAt(0); + diagnostic.ErrorCode.Should().Be(ErrorCodes.UnsupportedOperandType); + diagnostic.Message.Should().Be(Resources.UnsupporedOperandType.FormatInvariant("+", "int", "str")); + diagnostic.SourceSpan.Should().Be(2, 8, 2, 17); + diagnostic.Severity.Should().Be(Severity.Error); + } + + [DataRow("x = y #noqa")] + [DataRow("x = z + 2 #noqa")] + [DataTestMethod, Priority(0)] + public async Task IgnoreUndefinedVar(string code) { + var d = await LintAsync(code); + d.Should().BeEmpty(); + } + + [TestMethod, Priority(0)] + public async Task IgnoreMissingImport() { + string code = @" +from fake_module import User #noqa +"; + var analysis = await GetAnalysisAsync(code); + analysis.Diagnostics.Should().BeEmpty(); + } + + private async Task> LintAsync(string code, InterpreterConfiguration configuration = null) { + var analysis = await GetAnalysisAsync(code, configuration ?? PythonVersions.LatestAvailable3X); + var a = Services.GetService(); + return a.LintModule(analysis.Document); + } + } +}