Replace libdparse in UnusedResultChecker (#80)
* Replace libdparse with DMD in UnusedResultChecker * Run semantic analysis in unit test * Add more unit tests * Remove debug prints * Fix no return test * Remove semantic analysis
This commit is contained in:
parent
aece8d2bab
commit
99dcc4b794
|
|
@ -853,10 +853,6 @@ private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
|
||||||
checks ~= new IfConstraintsIndentCheck(args.setSkipTests(
|
checks ~= new IfConstraintsIndentCheck(args.setSkipTests(
|
||||||
analysisConfig.if_constraints_indent == Check.skipTests && !ut));
|
analysisConfig.if_constraints_indent == Check.skipTests && !ut));
|
||||||
|
|
||||||
if (moduleName.shouldRun!UnusedResultChecker(analysisConfig))
|
|
||||||
checks ~= new UnusedResultChecker(args.setSkipTests(
|
|
||||||
analysisConfig.unused_result == Check.skipTests && !ut));
|
|
||||||
|
|
||||||
return checks;
|
return checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1364,6 +1360,12 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
|
||||||
config.max_line_length
|
config.max_line_length
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (moduleName.shouldRunDmd!(UnusedResultChecker!ASTCodegen)(config))
|
||||||
|
visitors ~= new UnusedResultChecker!ASTCodegen(
|
||||||
|
fileName,
|
||||||
|
config.unused_result == Check.skipTests && !ut
|
||||||
|
);
|
||||||
|
|
||||||
foreach (visitor; visitors)
|
foreach (visitor; visitors)
|
||||||
{
|
{
|
||||||
m.accept(visitor);
|
m.accept(visitor);
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,7 @@
|
||||||
// http://www.boost.org/LICENSE_1_0.txt)
|
// http://www.boost.org/LICENSE_1_0.txt)
|
||||||
module dscanner.analysis.unused_result;
|
module dscanner.analysis.unused_result;
|
||||||
|
|
||||||
import dparse.ast;
|
|
||||||
import dparse.lexer;
|
|
||||||
import dscanner.analysis.base;
|
import dscanner.analysis.base;
|
||||||
import dscanner.analysis.mismatched_args : IdentVisitor, resolveSymbol;
|
|
||||||
import dscanner.utils;
|
|
||||||
import dsymbol.scope_;
|
|
||||||
import dsymbol.symbol;
|
|
||||||
import std.algorithm : canFind, countUntil;
|
|
||||||
import std.range : retro;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for function call statements which call non-void functions.
|
* Checks for function call statements which call non-void functions.
|
||||||
|
|
@ -24,105 +16,212 @@ import std.range : retro;
|
||||||
* When the return value is intentionally discarded, `cast(void)` can
|
* When the return value is intentionally discarded, `cast(void)` can
|
||||||
* be prepended to silence the check.
|
* be prepended to silence the check.
|
||||||
*/
|
*/
|
||||||
final class UnusedResultChecker : BaseAnalyzer
|
extern (C++) class UnusedResultChecker(AST) : BaseAnalyzerDmd
|
||||||
{
|
{
|
||||||
alias visit = BaseAnalyzer.visit;
|
alias visit = BaseAnalyzerDmd.visit;
|
||||||
|
mixin AnalyzerInfo!"unused_result";
|
||||||
|
|
||||||
mixin AnalyzerInfo!"unused_result";
|
private enum KEY = "dscanner.performance.enum_array_literal";
|
||||||
|
private enum string MSG = "Function return value is discarded";
|
||||||
|
|
||||||
private:
|
private bool[string] nonVoidFuncs;
|
||||||
|
private string[] aggregateStack;
|
||||||
|
|
||||||
enum string KEY = "dscanner.unused_result";
|
extern (D) this(string fileName, bool skipTests = false)
|
||||||
enum string MSG = "Function return value is discarded";
|
{
|
||||||
|
super(fileName, skipTests);
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
private template VisitAggregate(NodeType)
|
||||||
|
{
|
||||||
|
override void visit(NodeType aggregate)
|
||||||
|
{
|
||||||
|
string name = cast(string) aggregate.ident.toString();
|
||||||
|
aggregateStack ~= name;
|
||||||
|
super.visit(aggregate);
|
||||||
|
aggregateStack.length -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const(DSymbol)* void_;
|
mixin VisitAggregate!(AST.StructDeclaration);
|
||||||
const(DSymbol)* noreturn_;
|
mixin VisitAggregate!(AST.ClassDeclaration);
|
||||||
|
|
||||||
///
|
override void visit(AST.FuncDeclaration funcDeclaration)
|
||||||
this(BaseAnalyzerArguments args)
|
{
|
||||||
{
|
import dmd.astenums : TY;
|
||||||
super(args);
|
|
||||||
void_ = sc.getSymbolsByName(internString("void"))[0];
|
|
||||||
auto symbols = sc.getSymbolsByName(internString("noreturn"));
|
|
||||||
if (symbols.length > 0)
|
|
||||||
noreturn_ = symbols[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
override void visit(const(ExpressionStatement) decl)
|
auto typeFunc = funcDeclaration.type.isTypeFunction();
|
||||||
{
|
if (typeFunc && typeFunc.next && typeFunc.next.ty != TY.Tvoid && typeFunc.next.ty != TY.Tnoreturn)
|
||||||
import std.typecons : scoped;
|
{
|
||||||
|
auto typeIdent = typeFunc.next.isTypeIdentifier();
|
||||||
|
bool isNoReturn = typeIdent is null ? false : typeIdent.ident.toString() == "noreturn";
|
||||||
|
|
||||||
super.visit(decl);
|
if (!isNoReturn)
|
||||||
if (!decl.expression)
|
{
|
||||||
return;
|
string funcName = buildFullyQualifiedName(cast(string) funcDeclaration.ident.toString());
|
||||||
if (decl.expression.items.length != 1)
|
nonVoidFuncs[funcName] = true;
|
||||||
return;
|
}
|
||||||
auto ue = cast(UnaryExpression) decl.expression.items[0];
|
}
|
||||||
if (!ue)
|
|
||||||
return;
|
|
||||||
auto fce = ue.functionCallExpression;
|
|
||||||
if (!fce)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto identVisitor = scoped!IdentVisitor;
|
super.visit(funcDeclaration);
|
||||||
if (fce.unaryExpression !is null)
|
}
|
||||||
identVisitor.visit(fce.unaryExpression);
|
|
||||||
else if (fce.type !is null)
|
|
||||||
identVisitor.visit(fce.type);
|
|
||||||
|
|
||||||
if (!identVisitor.names.length)
|
override void visit(AST.AliasDeclaration aliasDecl)
|
||||||
return;
|
{
|
||||||
|
import std.algorithm : canFind, endsWith;
|
||||||
|
import std.array : replace;
|
||||||
|
|
||||||
const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names);
|
auto typeIdent = aliasDecl.type.isTypeIdentifier();
|
||||||
|
if (typeIdent is null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!symbols.length)
|
string aliasName = cast(string) aliasDecl.ident.toString();
|
||||||
return;
|
string targetName = cast(string) typeIdent.ident.toString();
|
||||||
|
|
||||||
foreach (sym; symbols)
|
foreach(func; nonVoidFuncs.byKey())
|
||||||
{
|
{
|
||||||
if (!sym)
|
if (func.endsWith(targetName) || func.canFind(targetName ~ "."))
|
||||||
return;
|
{
|
||||||
if (!sym.type)
|
string newAliasName = func.replace(targetName, aliasName);
|
||||||
return;
|
nonVoidFuncs[newAliasName] = true;
|
||||||
if (sym.kind != CompletionKind.functionName)
|
}
|
||||||
return;
|
}
|
||||||
if (sym.type is void_)
|
}
|
||||||
return;
|
|
||||||
if (noreturn_ && sym.type is noreturn_)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const(Token)[] tokens = fce.unaryExpression
|
private extern (D) string buildFullyQualifiedName(string funcName)
|
||||||
? fce.unaryExpression.tokens
|
{
|
||||||
: fce.type
|
import std.algorithm : fold;
|
||||||
? fce.type.tokens
|
|
||||||
: fce.tokens;
|
|
||||||
|
|
||||||
addErrorMessage(tokens, KEY, MSG);
|
if (aggregateStack.length == 0)
|
||||||
}
|
return funcName;
|
||||||
|
|
||||||
|
return aggregateStack.fold!((a, b) => a ~ "." ~ b) ~ "." ~ funcName;
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin VisitInstructionBlock!(AST.WhileStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.ForStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.DoStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.ForeachRangeStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.ForeachStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.SwitchStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.SynchronizedStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.WithStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.TryCatchStatement);
|
||||||
|
mixin VisitInstructionBlock!(AST.TryFinallyStatement);
|
||||||
|
|
||||||
|
override void visit(AST.CompoundStatement compoundStatement)
|
||||||
|
{
|
||||||
|
foreach (statement; *compoundStatement.statements)
|
||||||
|
{
|
||||||
|
if (hasUnusedResult(statement))
|
||||||
|
{
|
||||||
|
auto lineNum = cast(ulong) statement.loc.linnum;
|
||||||
|
auto charNum = cast(ulong) statement.loc.charnum;
|
||||||
|
addErrorMessage(lineNum, charNum, KEY, MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
statement.accept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override void visit(AST.IfStatement ifStatement)
|
||||||
|
{
|
||||||
|
if (hasUnusedResult(ifStatement.ifbody))
|
||||||
|
{
|
||||||
|
auto lineNum = cast(ulong) ifStatement.ifbody.loc.linnum;
|
||||||
|
auto charNum = cast(ulong) ifStatement.ifbody.loc.charnum;
|
||||||
|
addErrorMessage(lineNum, charNum, KEY, MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifStatement.elsebody && hasUnusedResult(ifStatement.elsebody))
|
||||||
|
{
|
||||||
|
auto lineNum = cast(ulong) ifStatement.elsebody.loc.linnum;
|
||||||
|
auto charNum = cast(ulong) ifStatement.elsebody.loc.charnum;
|
||||||
|
addErrorMessage(lineNum, charNum, KEY, MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.visit(ifStatement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private template VisitInstructionBlock(NodeType)
|
||||||
|
{
|
||||||
|
override void visit(NodeType statement)
|
||||||
|
{
|
||||||
|
if (hasUnusedResult(statement._body))
|
||||||
|
{
|
||||||
|
auto lineNum = cast(ulong) statement._body.loc.linnum;
|
||||||
|
auto charNum = cast(ulong) statement._body.loc.charnum;
|
||||||
|
addErrorMessage(lineNum, charNum, KEY, MSG);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.visit(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool hasUnusedResult(AST.Statement statement)
|
||||||
|
{
|
||||||
|
import dmd.astenums : TY;
|
||||||
|
|
||||||
|
auto exprStatement = statement.isExpStatement();
|
||||||
|
if (exprStatement is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto callExpr = exprStatement.exp.isCallExp();
|
||||||
|
if (callExpr is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string funcName = "";
|
||||||
|
|
||||||
|
if (auto identExpr = callExpr.e1.isIdentifierExp())
|
||||||
|
funcName = cast(string) identExpr.ident.toString();
|
||||||
|
else if (auto dotIdExpr = callExpr.e1.isDotIdExp())
|
||||||
|
funcName = buildFullyQualifiedCallName(dotIdExpr);
|
||||||
|
|
||||||
|
return (funcName in nonVoidFuncs) !is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extern (D) string buildFullyQualifiedCallName(AST.DotIdExp dotIdExpr)
|
||||||
|
{
|
||||||
|
import std.algorithm : fold, reverse;
|
||||||
|
|
||||||
|
string[] nameStack;
|
||||||
|
nameStack ~= cast(string) dotIdExpr.ident.toString();
|
||||||
|
|
||||||
|
auto lastExpr = dotIdExpr.e1;
|
||||||
|
while (lastExpr.isDotIdExp())
|
||||||
|
{
|
||||||
|
auto current = lastExpr.isDotIdExp();
|
||||||
|
nameStack ~= cast(string) current.ident.toString();
|
||||||
|
lastExpr = current.e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto identExpr = lastExpr.isIdentifierExp())
|
||||||
|
nameStack ~= cast(string) identExpr.ident.toString();
|
||||||
|
|
||||||
|
return nameStack.reverse.fold!((a, b) => a ~ "." ~ b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unittest
|
unittest
|
||||||
{
|
{
|
||||||
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
|
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
|
||||||
import dscanner.analysis.helpers : assertAnalyzerWarnings;
|
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD;
|
||||||
import std.stdio : stderr;
|
import std.stdio : stderr;
|
||||||
import std.format : format;
|
import std.format : format;
|
||||||
|
|
||||||
StaticAnalysisConfig sac = disabledConfig();
|
enum string MSG = "Function return value is discarded";
|
||||||
sac.unused_result = Check.enabled;
|
StaticAnalysisConfig sac = disabledConfig();
|
||||||
|
sac.unused_result = Check.enabled;
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
void fun() {}
|
void fun() {}
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
fun();
|
fun();
|
||||||
}
|
}
|
||||||
}c, sac);
|
}c, sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
alias noreturn = typeof(*null);
|
alias noreturn = typeof(*null);
|
||||||
noreturn fun() { while (1) {} }
|
noreturn fun() { while (1) {} }
|
||||||
noreturn main()
|
noreturn main()
|
||||||
|
|
@ -131,16 +230,15 @@ unittest
|
||||||
}
|
}
|
||||||
}c, sac);
|
}c, sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
int fun() { return 1; }
|
int fun() { return 1; }
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
fun(); /+
|
fun(); // [warn]: %s
|
||||||
^^^ [warn]: %s +/
|
|
||||||
}
|
}
|
||||||
}c.format(UnusedResultChecker.MSG), sac);
|
}c.format(MSG), sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
struct Foo
|
struct Foo
|
||||||
{
|
{
|
||||||
static bool get()
|
static bool get()
|
||||||
|
|
@ -151,12 +249,12 @@ unittest
|
||||||
alias Bar = Foo;
|
alias Bar = Foo;
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
Bar.get(); /+
|
Bar.get(); // [warn]: %s
|
||||||
^^^^^^^ [warn]: %s +/
|
Foo.bar.get();
|
||||||
}
|
}
|
||||||
}c.format(UnusedResultChecker.MSG), sac);
|
}c.format(MSG), sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
void fun() {}
|
void fun() {}
|
||||||
|
|
@ -164,17 +262,15 @@ unittest
|
||||||
}
|
}
|
||||||
}c, sac);
|
}c, sac);
|
||||||
|
|
||||||
version (none) // TODO: local functions
|
assertAnalyzerWarningsDMD(q{
|
||||||
assertAnalyzerWarnings(q{
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
int fun() { return 1; }
|
int fun() { return 1; }
|
||||||
fun(); /+
|
fun(); // [warn]: %s
|
||||||
^^^ [warn]: %s +/
|
|
||||||
}
|
}
|
||||||
}c.format(UnusedResultChecker.MSG), sac);
|
}c.format(MSG), sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
int fun() { return 1; }
|
int fun() { return 1; }
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
|
|
@ -182,7 +278,7 @@ unittest
|
||||||
}
|
}
|
||||||
}c, sac);
|
}c, sac);
|
||||||
|
|
||||||
assertAnalyzerWarnings(q{
|
assertAnalyzerWarningsDMD(q{
|
||||||
void fun() { }
|
void fun() { }
|
||||||
alias gun = fun;
|
alias gun = fun;
|
||||||
void main()
|
void main()
|
||||||
|
|
@ -191,7 +287,42 @@ unittest
|
||||||
}
|
}
|
||||||
}c, sac);
|
}c, sac);
|
||||||
|
|
||||||
import std.stdio: writeln;
|
assertAnalyzerWarningsDMD(q{
|
||||||
writeln("Unittest for UnusedResultChecker passed");
|
int fun() { return 1; }
|
||||||
}
|
void main()
|
||||||
|
{
|
||||||
|
if (true)
|
||||||
|
fun(); // [warn]: %s
|
||||||
|
else
|
||||||
|
fun(); // [warn]: %s
|
||||||
|
}
|
||||||
|
}c.format(MSG, MSG), sac);
|
||||||
|
|
||||||
|
assertAnalyzerWarningsDMD(q{
|
||||||
|
int fun() { return 1; }
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
fun(); // [warn]: %s
|
||||||
|
}
|
||||||
|
}c.format(MSG), sac);
|
||||||
|
|
||||||
|
assertAnalyzerWarningsDMD(q{
|
||||||
|
int fun() { return 1; }
|
||||||
|
alias gun = fun;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gun(); // [warn]: %s
|
||||||
|
}
|
||||||
|
}c.format(MSG), sac);
|
||||||
|
|
||||||
|
assertAnalyzerWarningsDMD(q{
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
void fun() {}
|
||||||
|
fun();
|
||||||
|
}
|
||||||
|
}c, sac);
|
||||||
|
|
||||||
|
stderr.writeln("Unittest for UnusedResultChecker passed");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue