Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.
Merged
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
3 changes: 1 addition & 2 deletions src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analysis/Ast/Impl/Analyzer/PythonAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public IReadOnlyList<DiagnosticsEntry> 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<IAnalysisOptionsProvider>();
return optionsProvider?.Options?.LintingEnabled == false ? Array.Empty<DiagnosticsEntry>() : result;
Expand Down
17 changes: 17 additions & 0 deletions src/Analysis/Ast/Impl/Diagnostics/DiagnosticsEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -51,6 +54,20 @@ public DiagnosticsEntry(string message, SourceSpan span, string errorCode, Sever
/// </summary>
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")) {
Comment thread
CTrando marked this conversation as resolved.
Comment thread
CTrando marked this conversation as resolved.
return false;
}

return true;
}

public override bool Equals(object obj) {
if (!(obj is DiagnosticsEntry e)) {
return false;
Expand Down
8 changes: 5 additions & 3 deletions src/Analysis/Ast/Impl/Extensions/AstExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@
using System.Diagnostics;
using System.Linq;
using Microsoft.Python.Analysis.Analyzer.Expressions;
using Microsoft.Python.Analysis.Modules;
Comment thread
CTrando marked this conversation as resolved.
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) {
Expand Down Expand Up @@ -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) {
Expand Down
35 changes: 35 additions & 0 deletions src/Analysis/Ast/Impl/Extensions/PythonModuleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,5 +27,39 @@ internal static T GetAstNode<T>(this IPythonModule module, object o) where T : N
=> ((IAstNodeContainer)module).GetAstNode<T>(o);
internal static void AddAstNode(this IPythonModule module, object o, Node n)
=> ((IAstNodeContainer)module).AddAstNode(o, n);

/// <summary>
/// Returns the string line corresponding to the given line number
/// </summary>
/// <param name="lineNum">The line number</param>
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', ' ');
}

/// <summary>
/// Returns the comment corresponding to the given line or an empty string if there is no comment
/// </summary>
/// <param name="lineNum">The line number</param>
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', ' ');
}
}
}
5 changes: 3 additions & 2 deletions src/Analysis/Ast/Impl/Linting/LinterAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,7 +28,7 @@ public LinterAggregator() {
// TODO: develop mechanism for dynamic and external linter discovery.
_linters.Add(new UndefinedVariablesLinter());
}
public IReadOnlyList<DiagnosticsEntry> Lint(IDocumentAnalysis analysis, IServiceContainer services)
=> _linters.SelectMany(l => l.Lint(analysis, services)).ToArray();
public IReadOnlyList<DiagnosticsEntry> Lint(IPythonModule module, IServiceContainer services)
=> _linters.SelectMany(l => l.Lint(module.Analysis, services)).Where(d => d.ShouldReport(module)).ToArray();
}
}
3 changes: 1 addition & 2 deletions src/Analysis/Ast/Impl/Modules/PythonModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,7 @@ public string Content {
return _buffer.Text;
}
}
}

}
#endregion

#region Parsing
Expand Down
120 changes: 120 additions & 0 deletions src/Analysis/Ast/Test/LintNoQATests.cs
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyList<DiagnosticsEntry>> LintAsync(string code, InterpreterConfiguration configuration = null) {
var analysis = await GetAnalysisAsync(code, configuration ?? PythonVersions.LatestAvailable3X);
var a = Services.GetService<IPythonAnalyzer>();
return a.LintModule(analysis.Document);
}
}
Comment thread
CTrando marked this conversation as resolved.
}