Skip to content

Commit

Permalink
Replace libdparse with DMD in UnusedVariableCheck
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladiwostok committed Apr 28, 2024
1 parent 81526df commit 9f86ff1
Show file tree
Hide file tree
Showing 2 changed files with 280 additions and 31 deletions.
10 changes: 6 additions & 4 deletions src/dscanner/analysis/run.d
Original file line number Diff line number Diff line change
Expand Up @@ -845,10 +845,6 @@ private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
checks ~= new UndocumentedDeclarationCheck(args.setSkipTests(
analysisConfig.undocumented_declaration_check == Check.skipTests && !ut));

if (moduleName.shouldRun!UnusedVariableCheck(analysisConfig))
checks ~= new UnusedVariableCheck(args.setSkipTests(
analysisConfig.unused_variable_check == Check.skipTests && !ut));

if (moduleName.shouldRun!UnusedParameterCheck(analysisConfig))
checks ~= new UnusedParameterCheck(args.setSkipTests(
analysisConfig.unused_parameter_check == Check.skipTests && !ut));
Expand Down Expand Up @@ -1350,6 +1346,12 @@ MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleN
config.auto_function_check == Check.skipTests && !ut
);

if (moduleName.shouldRunDmd!(UnusedVariableCheck!ASTCodegen)(config))
visitors ~= new UnusedVariableCheck!ASTCodegen(
fileName,
config.unused_variable_check == Check.skipTests && !ut
);

foreach (visitor; visitors)
{
m.accept(visitor);
Expand Down
301 changes: 274 additions & 27 deletions src/dscanner/analysis/unused_variable.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,282 @@
// http://www.boost.org/LICENSE_1_0.txt)
module dscanner.analysis.unused_variable;

import dparse.ast;
import dscanner.analysis.base;
import dscanner.analysis.unused;
import dsymbol.scope_ : Scope;
import std.algorithm.iteration : map;
import std.algorithm : all, canFind, each, endsWith, filter, map;

/**
* Checks for unused variables.
*/
final class UnusedVariableCheck : UnusedStorageCheck
// TODO: many similarities to unused_param.d, maybe refactor into a common base class
extern (C++) class UnusedVariableCheck(AST) : BaseAnalyzerDmd
{
alias visit = UnusedStorageCheck.visit;

alias visit = BaseAnalyzerDmd.visit;
mixin AnalyzerInfo!"unused_variable_check";

/**
* Params:
* fileName = the name of the file being analyzed
*/
this(BaseAnalyzerArguments args)
private enum KEY = "dscanner.suspicious.unused_variable";
private enum MSG = "Variable %s is never used.";

private static struct VarInfo
{
string name;
ulong lineNum;
ulong charNum;
bool isUsed = false;
}

private alias VarSet = VarInfo[string];
private VarSet[] usedVars;
private bool inMixin;
private bool shouldIgnoreDecls;
private bool inFunction;
private bool inAggregate;
private bool shouldNotPushScope;

extern (D) this(string fileName, bool skipTests = false)
{
super(fileName, skipTests);
pushScope();
}

override void visit(AST.FuncDeclaration funcDeclaration)
{
auto oldInFunction = inFunction;
inFunction = true;
super.visit(funcDeclaration);
inFunction = oldInFunction;
}

mixin VisitAggregate!(AST.ClassDeclaration);
mixin VisitAggregate!(AST.StructDeclaration);
mixin VisitAggregate!(AST.UnionDeclaration);

private template VisitAggregate(NodeType)
{
override void visit(NodeType node)
{
auto oldInFunction = inFunction;
auto oldInAggregate = inAggregate;
inFunction = false;
inAggregate = true;
super.visit(node);
inFunction = oldInFunction;
inAggregate = oldInAggregate;
}
}

mixin VisitConditional!(AST.ConditionalDeclaration);
mixin VisitConditional!(AST.ConditionalStatement);

private template VisitConditional(NodeType)
{
override void visit(NodeType node)
{
auto oldShouldNotPushScope = shouldNotPushScope;
shouldNotPushScope = true;
super.visit(node);
shouldNotPushScope = oldShouldNotPushScope;
}
}

override void visit(AST.CompoundStatement compoundStatement)
{
if (!shouldNotPushScope)
pushScope();

super.visit(compoundStatement);

if (!shouldNotPushScope)
popScope();
}

override void visit(AST.VarDeclaration varDeclaration)
{
super.visit(varDeclaration);

if (varDeclaration.ident)
{
string varName = cast(string) varDeclaration.ident.toString();
bool isAggregateField = inAggregate && !inFunction;
bool ignore = isAggregateField || shouldIgnoreDecls || varName.all!(c => c == '_');
currentScope[varName] = VarInfo(varName, varDeclaration.loc.linnum, varDeclaration.loc.charnum, ignore);
}
}

override void visit(AST.TypeAArray typeAArray)
{
import std.array : split;

super.visit(typeAArray);

string assocArrayStr = cast(string) typeAArray.toString();
assocArrayStr.split('[')
.filter!(key => key.endsWith(']'))
.map!(key => key.split(']')[0])
.each!(key => markAsUsed(key));
}

override void visit(AST.TemplateDeclaration templateDecl)
{
super(args, "Variable", "unused_variable");
super.visit(templateDecl);

if (templateDecl.ident)
{
string varName = cast(string) templateDecl.ident.toString();
bool isAggregateField = inAggregate && !inFunction;
bool ignore = isAggregateField || shouldIgnoreDecls || varName.all!(c => c == '_');
currentScope[varName] = VarInfo(varName, templateDecl.loc.linnum, templateDecl.loc.charnum, ignore);
}
}

override void visit(const VariableDeclaration variableDeclaration)
override void visit(AST.TypeSArray staticArray)
{
foreach (d; variableDeclaration.declarators)
this.variableDeclared(d.name.text, d.name, false);
variableDeclaration.accept(this);
if (auto identifierExpression = staticArray.dim.isIdentifierExp())
identifierExpression.accept(this);
}

override void visit(const AutoDeclaration autoDeclaration)
override void visit(AST.IdentifierExp identifierExp)
{
foreach (t; autoDeclaration.parts.map!(a => a.identifier))
this.variableDeclared(t.text, t, false);
autoDeclaration.accept(this);
if (identifierExp.ident)
markAsUsed(cast(string) identifierExp.ident.toString());

super.visit(identifierExp);
}

mixin VisitMixin!(AST.MixinExp);
mixin VisitMixin!(AST.MixinStatement);

private template VisitMixin(NodeType)
{
override void visit(NodeType node)
{
inMixin = true;
super.visit(node);
inMixin = false;
}
}

override void visit(AST.StringExp stringExp)
{
if (!inMixin)
return;

string str = cast(string) stringExp.toStringz();
currentScope.byKey
.filter!(param => canFind(str, param))
.each!(param => markAsUsed(param));
}

override void visit(AST.TraitsExp traitsExp)
{
import dmd.dtemplate : isType;

auto oldShouldIgnoreDecls = shouldIgnoreDecls;

if (cast(string) traitsExp.ident.toString() == "compiles")
shouldIgnoreDecls = true;

super.visit(traitsExp);
shouldIgnoreDecls = oldShouldIgnoreDecls;

if (traitsExp.args is null)
return;

(*traitsExp.args).opSlice().map!(arg => isType(arg))
.filter!(type => type !is null)
.map!(type => type.isTypeIdentifier())
.filter!(typeIdentifier => typeIdentifier !is null)
.each!(typeIdentifier => markAsUsed(cast(string) typeIdentifier.toString()));
}

override void visit(AST.TypeTypeof typeOf)
{
auto oldShouldIgnoreDecls = shouldIgnoreDecls;
shouldIgnoreDecls = true;
super.visit(typeOf);
shouldIgnoreDecls = oldShouldIgnoreDecls;
}

override void visit(AST.TemplateInstance templateInstance)
{
import dmd.dtemplate : isExpression, isType;
import dmd.mtype : Type;

super.visit(templateInstance);

if (templateInstance.name)
markAsUsed(cast(string) templateInstance.name.toString());

if (templateInstance.tiargs is null)
return;

auto argSlice = (*templateInstance.tiargs).opSlice();

argSlice.map!(arg => arg.isExpression())
.filter!(arg => arg !is null)
.map!(arg => arg.isIdentifierExp())
.filter!(identifierExp => identifierExp !is null)
.each!(identifierExp => markAsUsed(cast(string) identifierExp.ident.toString()));

argSlice.map!(arg => arg.isType())
.filter!(arg => arg !is null)
.map!(arg => arg.isTypeIdentifier())
.filter!(identifierType => identifierType !is null)
.each!(identifierType => markAsUsed(cast(string) identifierType.ident.toString()));
}

private extern (D) void markAsUsed(string varName)
{
import std.range : retro;

foreach (funcScope; usedVars.retro())
{
if (varName in funcScope)
{
funcScope[varName].isUsed = true;
break;
}
}
}

@property private extern (D) VarSet currentScope()
{
return usedVars[$ - 1];
}

private void pushScope()
{
// Error with gdc-12
//usedVars ~= new VarSet;

// Workaround for gdc-12
VarSet newScope;
newScope["test"] = VarInfo("test", 0, 0);
usedVars ~= newScope;
currentScope.remove("test");
}

private void popScope()
{
import std.format : format;

currentScope.byValue
.filter!(var => !var.isUsed)
.each!(var => addErrorMessage(var.lineNum, var.charNum, KEY, MSG.format(var.name)));

usedVars.length--;
}
}

@system unittest
{
import std.stdio : stderr;
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings;
import dscanner.analysis.helpers : assertAnalyzerWarningsDMD;

StaticAnalysisConfig sac = disabledConfig();
sac.unused_variable_check = Check.enabled;
assertAnalyzerWarnings(q{

assertAnalyzerWarningsDMD(q{

// Issue 274
unittest
Expand All @@ -62,8 +290,7 @@ final class UnusedVariableCheck : UnusedStorageCheck

unittest
{
int a; /+
^ [warn]: Variable a is never used. +/
int a; // [warn]: Variable a is never used.
}

// Issue 380
Expand All @@ -76,8 +303,7 @@ final class UnusedVariableCheck : UnusedStorageCheck
// Issue 380
int otherTemplatedEnum()
{
auto a(T) = T.init; /+
^ [warn]: Variable a is never used. +/
auto a(T) = T.init; // [warn]: Variable a is never used.
return 0;
}

Expand Down Expand Up @@ -132,5 +358,26 @@ final class UnusedVariableCheck : UnusedStorageCheck
}

}c, sac);

assertAnalyzerWarningsDMD(q{
void testMixinExpression()
{
int a;
mixin("a = 5");
}
}c, sac);

assertAnalyzerWarningsDMD(q{
bool f()
{
static if (is(S == bool) && is(typeof({ T s = "string"; })))
{
return src ? "true" : "false";
}

return false;
}
}c, sac);

stderr.writeln("Unittest for UnusedVariableCheck passed.");
}

0 comments on commit 9f86ff1

Please sign in to comment.