Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow skipping checks with @("nolint(...)") and @nolint("...") #936

Merged
merged 17 commits into from
Oct 13, 2023
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
51 changes: 49 additions & 2 deletions src/dscanner/analysis/base.d
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module dscanner.analysis.base;

import dparse.ast;
import dparse.lexer : IdType, str, Token;
import dparse.lexer : IdType, str, Token, tok;
import dscanner.analysis.nolint;
import dsymbol.scope_ : Scope;
import std.array;
import std.container;
Expand Down Expand Up @@ -405,6 +406,35 @@ public:
unittest_.accept(this);
}

/**
* Visits a module declaration.
*
* When overriden, make sure to keep this structure
*/
override void visit(const(Module) mod)
{
if (mod.moduleDeclaration !is null)
{
with (noLint.push(NoLintFactory.fromModuleDeclaration(mod.moduleDeclaration)))
mod.accept(this);
}
else
{
mod.accept(this);
}
}

/**
* Visits a declaration.
*
* When overriden, make sure to disable and reenable error messages
*/
override void visit(const(Declaration) decl)
{
with (noLint.push(NoLintFactory.fromDeclaration(decl)))
decl.accept(this);
}

AutoFix.CodeReplacement[] resolveAutoFix(
const Module mod,
scope const(Token)[] tokens,
Expand All @@ -423,6 +453,7 @@ protected:

bool inAggregate;
bool skipTests;
NoLint noLint;

template visitTemplate(T)
{
Expand All @@ -437,42 +468,58 @@ protected:
deprecated("Use the overload taking start and end locations or a Node instead")
void addErrorMessage(size_t line, size_t column, string key, string message)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(fileName, line, column, key, message, getName()));
}

void addErrorMessage(const BaseNode node, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, node, message), key, autofixes);
}

void addErrorMessage(const Token token, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, token, message), key, autofixes);
}

void addErrorMessage(const Token[] tokens, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, tokens, message), key, autofixes);
}

void addErrorMessage(size_t[2] index, size_t line, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(index, [line, line], columns, key, message, autofixes);
}

void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
auto d = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(d, key, getName(), autofixes));
}

void addErrorMessage(Message.Diagnostic diagnostic, string key, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, key, getName(), autofixes));
}

void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, supplemental, key, getName(), autofixes));
}

Expand Down Expand Up @@ -756,7 +803,7 @@ unittest
testScopes(q{
auto isNewScope = void;
auto depth = 1;

void foo() {
isNewScope();
isOldScope();
Expand Down
271 changes: 271 additions & 0 deletions src/dscanner/analysis/nolint.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
module dscanner.analysis.nolint;

@safe:

import dparse.ast;
import dparse.lexer;

import std.algorithm : canFind;
import std.regex : matchAll, regex;
import std.string : lastIndexOf, strip;
import std.typecons;

struct NoLint
{
bool containsCheck(scope const(char)[] check) const
{
while (true)
{
if (disabledChecks.get((() @trusted => cast(string) check)(), 0) > 0)
return true;

auto dot = check.lastIndexOf('.');
if (dot == -1)
break;
check = check[0 .. dot];
}
return false;
}

// automatic pop when returned value goes out of scope
Poppable push(in Nullable!NoLint other) scope
{
if (other.isNull)
return Poppable(null);

foreach (key, value; other.get.getDisabledChecks)
this.disabledChecks[key] += value;

return Poppable(() => this.pop(other));
}

package:
const(int[string]) getDisabledChecks() const
{
return this.disabledChecks;
}

void pushCheck(in string check)
{
disabledChecks[check]++;
}

void merge(in Nullable!NoLint other)
{
if (other.isNull)
return;

foreach (key, value; other.get.getDisabledChecks)
this.disabledChecks[key] += value;
}

private:
void pop(in Nullable!NoLint other)
{
if (other.isNull)
return;

foreach (key, value; other.get.getDisabledChecks)
{
assert(this.disabledChecks.get(key, 0) >= value);

this.disabledChecks[key] -= value;
}
}

static struct Poppable
{
~this()
{
if (onPop)
onPop();
onPop = null;
}

private:
void delegate() onPop;
}

int[string] disabledChecks;
}

struct NoLintFactory
{
static Nullable!NoLint fromModuleDeclaration(in ModuleDeclaration moduleDeclaration)
{
NoLint noLint;

foreach (atAttribute; moduleDeclaration.atAttributes)
noLint.merge(NoLintFactory.fromAtAttribute(atAttribute));

if (!noLint.getDisabledChecks.length)
return nullNoLint;

return noLint.nullable;
}

static Nullable!NoLint fromDeclaration(in Declaration declaration)
{
NoLint noLint;
foreach (attribute; declaration.attributes)
noLint.merge(NoLintFactory.fromAttribute(attribute));

if (!noLint.getDisabledChecks.length)
return nullNoLint;

return noLint.nullable;
}

private:
static Nullable!NoLint fromAttribute(const(Attribute) attribute)
{
if (attribute is null)
return nullNoLint;

return NoLintFactory.fromAtAttribute(attribute.atAttribute);

}

static Nullable!NoLint fromAtAttribute(const(AtAttribute) atAttribute)
{
if (atAttribute is null)
return nullNoLint;

auto ident = atAttribute.identifier;
auto argumentList = atAttribute.argumentList;

if (argumentList !is null)
{
if (ident.text.length)
return NoLintFactory.fromStructUda(ident, argumentList);
else
return NoLintFactory.fromStringUda(argumentList);

}
else
return nullNoLint;
}

// @nolint("..")
static Nullable!NoLint fromStructUda(in Token ident, in ArgumentList argumentList)
in (ident.text.length && argumentList !is null)
{
if (ident.text != "nolint")
return nullNoLint;

NoLint noLint;

foreach (nodeExpr; argumentList.items)
{
if (auto unaryExpr = cast(const UnaryExpression) nodeExpr)
{
auto primaryExpression = unaryExpr.primaryExpression;
if (primaryExpression is null)
continue;

if (primaryExpression.primary != tok!"stringLiteral")
continue;

noLint.pushCheck(primaryExpression.primary.text.strip("\""));
}
}

if (!noLint.getDisabledChecks().length)
return nullNoLint;

return noLint.nullable;
}

// @("nolint(..)")
static Nullable!NoLint fromStringUda(in ArgumentList argumentList)
in (argumentList !is null)
{
NoLint noLint;

foreach (nodeExpr; argumentList.items)
{
if (auto unaryExpr = cast(const UnaryExpression) nodeExpr)
{
auto primaryExpression = unaryExpr.primaryExpression;
if (primaryExpression is null)
continue;

if (primaryExpression.primary != tok!"stringLiteral")
continue;

auto str = primaryExpression.primary.text.strip("\"");
Nullable!NoLint currNoLint = NoLintFactory.fromString(str);
noLint.merge(currNoLint);
}
}

if (!noLint.getDisabledChecks().length)
return nullNoLint;

return noLint.nullable;

}

// Transform a string with form "nolint(abc, efg)"
// into a NoLint struct
static Nullable!NoLint fromString(in string str)
{
static immutable re = regex(`[\w-_.]+`, "g");
auto matches = matchAll(str, re);

if (!matches)
return nullNoLint;

const udaName = matches.hit;
if (udaName != "nolint")
return nullNoLint;

matches.popFront;

NoLint noLint;

while (matches)
{
noLint.pushCheck(matches.hit);
matches.popFront;
}

if (!noLint.getDisabledChecks.length)
return nullNoLint;

return noLint.nullable;
}

static nullNoLint = Nullable!NoLint.init;
}

unittest
{
const s1 = "nolint(abc)";
const s2 = "nolint(abc, efg, hij)";
const s3 = " nolint ( abc , efg ) ";
const s4 = "nolint(dscanner.style.abc_efg-ijh)";
const s5 = "OtherUda(abc)";
const s6 = "nolint(dscanner)";

assert(NoLintFactory.fromString(s1).get.containsCheck("abc"));

assert(NoLintFactory.fromString(s2).get.containsCheck("abc"));
assert(NoLintFactory.fromString(s2).get.containsCheck("efg"));
assert(NoLintFactory.fromString(s2).get.containsCheck("hij"));

assert(NoLintFactory.fromString(s3).get.containsCheck("abc"));
assert(NoLintFactory.fromString(s3).get.containsCheck("efg"));

assert(NoLintFactory.fromString(s4).get.containsCheck("dscanner.style.abc_efg-ijh"));

assert(NoLintFactory.fromString(s5).isNull);

assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner"));
assert(!NoLintFactory.fromString(s6).get.containsCheck("dscanner2"));
assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner.foo"));

import std.stdio : stderr, writeln;

(() @trusted => stderr.writeln("Unittest for NoLint passed."))();
}
Loading