mirror of
https://github.com/dlang-community/D-Scanner.git
synced 2025-04-26 21:30:14 +03:00
Separate analyze with dmd and autofix flows from libdparse analyze flow (#142)
* Separate analyze with dmd and autofix flows from libdparse analyze flow * Make locally used functions private * Extract parsing using DMD in a separate function * Address feedback
This commit is contained in:
parent
531f75bd29
commit
c0c881ed39
6 changed files with 763 additions and 1129 deletions
340
src/dscanner/analysis/autofix.d
Normal file
340
src/dscanner/analysis/autofix.d
Normal file
|
@ -0,0 +1,340 @@
|
|||
module dscanner.analysis.autofix;
|
||||
|
||||
import std.algorithm : filter, findSplit;
|
||||
import std.conv : to;
|
||||
import std.functional : toDelegate;
|
||||
import std.stdio;
|
||||
|
||||
import dparse.lexer;
|
||||
import dparse.rollback_allocator;
|
||||
import dparse.ast : Module;
|
||||
|
||||
import dsymbol.modulecache : ModuleCache;
|
||||
|
||||
import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzer, Message;
|
||||
import dscanner.analysis.config : StaticAnalysisConfig;
|
||||
import dscanner.analysis.run : analyze, doNothing;
|
||||
import dscanner.utils : readFile, readStdin;
|
||||
|
||||
private void resolveAutoFixes(
|
||||
ref Message message,
|
||||
string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens,
|
||||
const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid
|
||||
)
|
||||
{
|
||||
resolveAutoFixes(message.checkName, message.autofixes, fileName, moduleCache,
|
||||
tokens, m, analysisConfig, overrideFormattingConfig);
|
||||
}
|
||||
|
||||
private void resolveAutoFixes(string messageCheckName, AutoFix[] autofixes, string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
import core.memory : GC;
|
||||
import dsymbol.conversion.first : FirstPass;
|
||||
import dsymbol.conversion.second : secondPass;
|
||||
import dsymbol.scope_ : Scope;
|
||||
import dsymbol.semantic : SemanticSymbol;
|
||||
import dsymbol.string_interning : internString;
|
||||
import dsymbol.symbol : DSymbol;
|
||||
import dscanner.analysis.run : getAnalyzersForModuleAndConfig;
|
||||
|
||||
const(AutoFixFormatting) formattingConfig =
|
||||
overrideFormattingConfig is AutoFixFormatting.invalid
|
||||
? analysisConfig.getAutoFixFormattingConfig()
|
||||
: overrideFormattingConfig;
|
||||
|
||||
scope first = new FirstPass(m, internString(fileName), &moduleCache, null);
|
||||
first.run();
|
||||
|
||||
secondPass(first.rootSymbol, first.moduleScope, moduleCache);
|
||||
auto moduleScope = first.moduleScope;
|
||||
scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol);
|
||||
scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol);
|
||||
scope(exit) typeid(Scope).destroy(first.moduleScope);
|
||||
|
||||
GC.disable;
|
||||
scope (exit)
|
||||
GC.enable;
|
||||
|
||||
foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope))
|
||||
{
|
||||
if (check.getName() == messageCheckName)
|
||||
{
|
||||
foreach (ref autofix; autofixes)
|
||||
autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Cannot find analyzer " ~ messageCheckName
|
||||
~ " to resolve autofix with.");
|
||||
}
|
||||
|
||||
void resolveAutoFixFromCheck(
|
||||
ref AutoFix autofix,
|
||||
BaseAnalyzer check,
|
||||
const Module m,
|
||||
scope const(Token)[] tokens,
|
||||
const AutoFixFormatting formattingConfig
|
||||
)
|
||||
{
|
||||
import std.sumtype : match;
|
||||
|
||||
autofix.replacements.match!(
|
||||
(AutoFix.ResolveContext context) {
|
||||
autofix.replacements = check.resolveAutoFix(m, tokens, context, formattingConfig);
|
||||
},
|
||||
(_) {}
|
||||
);
|
||||
}
|
||||
|
||||
private AutoFix.CodeReplacement[] resolveAutoFix(string messageCheckName, AutoFix.ResolveContext context,
|
||||
string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
AutoFix temp;
|
||||
temp.replacements = context;
|
||||
resolveAutoFixes(messageCheckName, (&temp)[0 .. 1], fileName, moduleCache,
|
||||
tokens, m, analysisConfig, overrideFormattingConfig);
|
||||
return temp.expectReplacements("resolving didn't work?!");
|
||||
}
|
||||
|
||||
void listAutofixes(
|
||||
StaticAnalysisConfig config,
|
||||
string resolveMessage,
|
||||
bool usingStdin,
|
||||
string fileName,
|
||||
StringCache* cache,
|
||||
ref ModuleCache moduleCache
|
||||
)
|
||||
{
|
||||
import dparse.parser : parseModule;
|
||||
import dscanner.analysis.base : Message;
|
||||
import std.format : format;
|
||||
import std.json : JSONValue;
|
||||
|
||||
union RequestedLocation
|
||||
{
|
||||
struct
|
||||
{
|
||||
uint line, column;
|
||||
}
|
||||
ulong bytes;
|
||||
}
|
||||
|
||||
RequestedLocation req;
|
||||
bool isBytes = resolveMessage[0] == 'b';
|
||||
if (isBytes)
|
||||
req.bytes = resolveMessage[1 .. $].to!ulong;
|
||||
else
|
||||
{
|
||||
auto parts = resolveMessage.findSplit(":");
|
||||
req.line = parts[0].to!uint;
|
||||
req.column = parts[2].to!uint;
|
||||
}
|
||||
|
||||
bool matchesCursor(Message m)
|
||||
{
|
||||
return isBytes
|
||||
? req.bytes >= m.startIndex && req.bytes <= m.endIndex
|
||||
: req.line >= m.startLine && req.line <= m.endLine
|
||||
&& (req.line > m.startLine || req.column >= m.startColumn)
|
||||
&& (req.line < m.endLine || req.column <= m.endColumn);
|
||||
}
|
||||
|
||||
RollbackAllocator rba;
|
||||
LexerConfig lexerConfig;
|
||||
lexerConfig.fileName = fileName;
|
||||
lexerConfig.stringBehavior = StringBehavior.source;
|
||||
auto tokens = getTokensForParser(usingStdin ? readStdin()
|
||||
: readFile(fileName), lexerConfig, cache);
|
||||
auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing));
|
||||
|
||||
auto messages = analyze(fileName, mod, config, moduleCache, tokens);
|
||||
|
||||
with (stdout.lockingTextWriter)
|
||||
{
|
||||
put("[");
|
||||
foreach (message; messages[].filter!matchesCursor)
|
||||
{
|
||||
resolveAutoFixes(message, fileName, moduleCache, tokens, mod, config);
|
||||
|
||||
foreach (i, autofix; message.autofixes)
|
||||
{
|
||||
put(i == 0 ? "\n" : ",\n");
|
||||
put("\t{\n");
|
||||
put(format!"\t\t\"name\": %s,\n"(JSONValue(autofix.name)));
|
||||
put("\t\t\"replacements\": [");
|
||||
foreach (j, replacement; autofix.expectReplacements)
|
||||
{
|
||||
put(j == 0 ? "\n" : ",\n");
|
||||
put(format!"\t\t\t{\"range\": [%d, %d], \"newText\": %s}"(
|
||||
replacement.range[0],
|
||||
replacement.range[1],
|
||||
JSONValue(replacement.newText)));
|
||||
}
|
||||
put("\n");
|
||||
put("\t\t]\n");
|
||||
put("\t}");
|
||||
}
|
||||
}
|
||||
put("\n]");
|
||||
}
|
||||
stdout.flush();
|
||||
}
|
||||
|
||||
void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements)
|
||||
{
|
||||
import std.algorithm : endsWith, startsWith;
|
||||
import std.ascii : isWhite;
|
||||
import std.string : strip;
|
||||
import std.utf : stride, strideBack;
|
||||
|
||||
enum WS
|
||||
{
|
||||
none, tab, space, newline
|
||||
}
|
||||
|
||||
WS getWS(size_t i)
|
||||
{
|
||||
if (cast(ptrdiff_t) i < 0 || i >= code.length)
|
||||
return WS.newline;
|
||||
switch (code[i])
|
||||
{
|
||||
case '\n':
|
||||
case '\r':
|
||||
return WS.newline;
|
||||
case '\t':
|
||||
return WS.tab;
|
||||
case ' ':
|
||||
return WS.space;
|
||||
default:
|
||||
return WS.none;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ref replacement; replacements)
|
||||
{
|
||||
assert(replacement.range[0] >= 0 && replacement.range[0] < code.length
|
||||
&& replacement.range[1] >= 0 && replacement.range[1] < code.length
|
||||
&& replacement.range[0] <= replacement.range[1], "trying to autofix whitespace on code that doesn't match with what the replacements were generated for");
|
||||
|
||||
void growRight()
|
||||
{
|
||||
// this is basically: replacement.range[1]++;
|
||||
if (code[replacement.range[1] .. $].startsWith("\r\n"))
|
||||
replacement.range[1] += 2;
|
||||
else if (replacement.range[1] < code.length)
|
||||
replacement.range[1] += code.stride(replacement.range[1]);
|
||||
}
|
||||
|
||||
void growLeft()
|
||||
{
|
||||
// this is basically: replacement.range[0]--;
|
||||
if (code[0 .. replacement.range[0]].endsWith("\r\n"))
|
||||
replacement.range[0] -= 2;
|
||||
else if (replacement.range[0] > 0)
|
||||
replacement.range[0] -= code.strideBack(replacement.range[0]);
|
||||
}
|
||||
|
||||
if (replacement.newText.strip.length)
|
||||
{
|
||||
if (replacement.newText.startsWith(" "))
|
||||
{
|
||||
// we insert with leading space, but there is a space/NL/SOF before
|
||||
// remove to-be-inserted space
|
||||
if (getWS(replacement.range[0] - 1))
|
||||
replacement.newText = replacement.newText[1 .. $];
|
||||
}
|
||||
if (replacement.newText.startsWith("]", ")"))
|
||||
{
|
||||
// when inserting `)`, consume regular space before
|
||||
if (getWS(replacement.range[0] - 1) == WS.space)
|
||||
growLeft();
|
||||
}
|
||||
if (replacement.newText.endsWith(" "))
|
||||
{
|
||||
// we insert with trailing space, but there is a space/NL/EOF after, chomp off
|
||||
if (getWS(replacement.range[1]))
|
||||
replacement.newText = replacement.newText[0 .. $ - 1];
|
||||
}
|
||||
if (replacement.newText.endsWith("[", "("))
|
||||
{
|
||||
if (getWS(replacement.range[1]))
|
||||
growRight();
|
||||
}
|
||||
}
|
||||
else if (!replacement.newText.length)
|
||||
{
|
||||
// after removing code and ending up with whitespace on both sides,
|
||||
// collapse 2 whitespace into one
|
||||
switch (getWS(replacement.range[1]))
|
||||
{
|
||||
case WS.newline:
|
||||
switch (getWS(replacement.range[0] - 1))
|
||||
{
|
||||
case WS.newline:
|
||||
// after removal we have NL ~ NL or SOF ~ NL,
|
||||
// remove right NL
|
||||
growRight();
|
||||
break;
|
||||
case WS.space:
|
||||
case WS.tab:
|
||||
// after removal we have space ~ NL,
|
||||
// remove the space
|
||||
growLeft();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case WS.space:
|
||||
case WS.tab:
|
||||
// for NL ~ space, SOF ~ space, space ~ space, tab ~ space,
|
||||
// for NL ~ tab, SOF ~ tab, space ~ tab, tab ~ tab
|
||||
// remove right space/tab
|
||||
if (getWS(replacement.range[0] - 1))
|
||||
growRight();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
import std.algorithm : sort;
|
||||
|
||||
AutoFix.CodeReplacement r(int start, int end, string s)
|
||||
{
|
||||
return AutoFix.CodeReplacement([start, end], s);
|
||||
}
|
||||
|
||||
string test(string code, AutoFix.CodeReplacement[] replacements...)
|
||||
{
|
||||
replacements.sort!"a.range[0] < b.range[0]";
|
||||
improveAutoFixWhitespace(code, replacements);
|
||||
foreach_reverse (r; replacements)
|
||||
code = code[0 .. r.range[0]] ~ r.newText ~ code[r.range[1] .. $];
|
||||
return code;
|
||||
}
|
||||
|
||||
assert(test("import a;\nimport b;", r(0, 9, "")) == "import b;");
|
||||
assert(test("import a;\r\nimport b;", r(0, 9, "")) == "import b;");
|
||||
assert(test("import a;\nimport b;", r(8, 9, "")) == "import a\nimport b;");
|
||||
assert(test("import a;\nimport b;", r(7, 8, "")) == "import ;\nimport b;");
|
||||
assert(test("import a;\r\nimport b;", r(7, 8, "")) == "import ;\r\nimport b;");
|
||||
assert(test("a b c", r(2, 3, "")) == "a c");
|
||||
}
|
|
@ -238,6 +238,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
|
|||
string file = __FILE__, size_t line = __LINE__)
|
||||
{
|
||||
import dparse.lexer : StringCache, Token;
|
||||
import dscanner.analysis.autofix : improveAutoFixWhitespace;
|
||||
import dscanner.analysis.run : parseModule;
|
||||
import std.algorithm : canFind, findSplit, map, sort;
|
||||
import std.conv : to;
|
||||
|
@ -359,43 +360,28 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi
|
|||
void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, bool semantic = false,
|
||||
string file = __FILE__, size_t line = __LINE__)
|
||||
{
|
||||
import dmd.globals : global;
|
||||
import dscanner.utils : getModuleName;
|
||||
import std.file : remove, exists;
|
||||
import std.stdio : File;
|
||||
import std.path : dirName;
|
||||
import dmd.arraytypes : Strings;
|
||||
|
||||
import std.stdio : File;
|
||||
import std.file : exists, remove;
|
||||
import std.path : dirName;
|
||||
import std.stdio : File;
|
||||
import dscanner.analysis.rundmd : analyzeDmd, parseDmdModule;
|
||||
import dscanner.utils : getModuleName;
|
||||
|
||||
auto deleteme = "test.txt";
|
||||
File f = File(deleteme, "w");
|
||||
auto testFileName = "test.d";
|
||||
File f = File(testFileName, "w");
|
||||
scope(exit)
|
||||
{
|
||||
assert(exists(deleteme));
|
||||
remove(deleteme);
|
||||
assert(exists(testFileName));
|
||||
remove(testFileName);
|
||||
}
|
||||
|
||||
f.write(code);
|
||||
f.close();
|
||||
|
||||
auto dmdParentDir = dirName(dirName(dirName(dirName(__FILE_FULL_PATH__))));
|
||||
|
||||
global.params.useUnitTests = true;
|
||||
global.path = Strings();
|
||||
global.path.push((dmdParentDir ~ "/dmd" ~ "\0").ptr);
|
||||
global.path.push((dmdParentDir ~ "/dmd/druntime/src" ~ "\0").ptr);
|
||||
|
||||
initDMD();
|
||||
|
||||
auto input = cast(char[]) code;
|
||||
input ~= '\0';
|
||||
auto t = dmd.frontend.parseModule(cast(const(char)[]) file, cast(const (char)[]) input);
|
||||
auto dmdModule = parseDmdModule(file, code);
|
||||
if (semantic)
|
||||
t.module_.fullSemantic();
|
||||
dmdModule.fullSemantic();
|
||||
|
||||
MessageSet rawWarnings = analyzeDmd("test.txt", t.module_, getModuleName(t.module_.md), config);
|
||||
MessageSet rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config);
|
||||
|
||||
string[] codeLines = code.splitLines();
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import dparse.ast;
|
|||
import dparse.lexer;
|
||||
import dparse.parser;
|
||||
import dparse.rollback_allocator;
|
||||
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
import std.conv;
|
||||
|
@ -26,6 +27,7 @@ import std.experimental.allocator.mallocator : Mallocator;
|
|||
import std.experimental.allocator.building_blocks.region : Region;
|
||||
import std.experimental.allocator.building_blocks.allocator_list : AllocatorList;
|
||||
|
||||
import dscanner.analysis.autofix : improveAutoFixWhitespace;
|
||||
import dscanner.analysis.config;
|
||||
import dscanner.analysis.base;
|
||||
import dscanner.analysis.style;
|
||||
|
@ -390,37 +392,21 @@ void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAna
|
|||
bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat,
|
||||
ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true)
|
||||
{
|
||||
import dmd.parse : Parser;
|
||||
import dmd.astbase : ASTBase;
|
||||
import dmd.id : Id;
|
||||
import dmd.globals : global;
|
||||
import dmd.identifier : Identifier;
|
||||
import std.string : toStringz;
|
||||
import dmd.arraytypes : Strings;
|
||||
import dscanner.analysis.rundmd : parseDmdModule;
|
||||
|
||||
import dscanner.analysis.rundmd : analyzeDmd;
|
||||
|
||||
bool hasErrors;
|
||||
foreach (fileName; fileNames)
|
||||
{
|
||||
|
||||
auto dmdParentDir = dirName(dirName(dirName(dirName(__FILE_FULL_PATH__))));
|
||||
|
||||
global.params.useUnitTests = true;
|
||||
global.path = Strings();
|
||||
global.path.push((dmdParentDir ~ "/dmd" ~ "\0").ptr);
|
||||
global.path.push((dmdParentDir ~ "/dmd/druntime/src" ~ "\0").ptr);
|
||||
|
||||
initDMD();
|
||||
|
||||
auto code = readFile(fileName);
|
||||
auto input = cast(char[]) code;
|
||||
input ~= '\0';
|
||||
|
||||
auto t = dmd.frontend.parseModule(cast(const(char)[]) fileName, cast(const (char)[]) input);
|
||||
// t.module_.fullSemantic();
|
||||
|
||||
// Skip files that could not be read and continue with the rest
|
||||
if (code.length == 0)
|
||||
continue;
|
||||
|
||||
auto dmdModule = parseDmdModule(fileName, cast(string) code);
|
||||
|
||||
RollbackAllocator r;
|
||||
uint errorCount;
|
||||
uint warningCount;
|
||||
|
@ -431,7 +417,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error
|
|||
if (errorCount > 0 || (staticAnalyze && warningCount > 0))
|
||||
hasErrors = true;
|
||||
MessageSet results = analyze(fileName, m, config, moduleCache, tokens, staticAnalyze);
|
||||
MessageSet resultsDmd = analyzeDmd(fileName, t.module_, getModuleName(t.module_.md), config);
|
||||
MessageSet resultsDmd = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config);
|
||||
foreach (result; resultsDmd[])
|
||||
{
|
||||
results.insert(result);
|
||||
|
@ -521,90 +507,6 @@ bool autofix(string[] fileNames, const StaticAnalysisConfig config, string error
|
|||
return hasErrors;
|
||||
}
|
||||
|
||||
void listAutofixes(
|
||||
StaticAnalysisConfig config,
|
||||
string resolveMessage,
|
||||
bool usingStdin,
|
||||
string fileName,
|
||||
StringCache* cache,
|
||||
ref ModuleCache moduleCache
|
||||
)
|
||||
{
|
||||
import dparse.parser : parseModule;
|
||||
import dscanner.analysis.base : Message;
|
||||
import std.format : format;
|
||||
import std.json : JSONValue;
|
||||
|
||||
union RequestedLocation
|
||||
{
|
||||
struct
|
||||
{
|
||||
uint line, column;
|
||||
}
|
||||
ulong bytes;
|
||||
}
|
||||
|
||||
RequestedLocation req;
|
||||
bool isBytes = resolveMessage[0] == 'b';
|
||||
if (isBytes)
|
||||
req.bytes = resolveMessage[1 .. $].to!ulong;
|
||||
else
|
||||
{
|
||||
auto parts = resolveMessage.findSplit(":");
|
||||
req.line = parts[0].to!uint;
|
||||
req.column = parts[2].to!uint;
|
||||
}
|
||||
|
||||
bool matchesCursor(Message m)
|
||||
{
|
||||
return isBytes
|
||||
? req.bytes >= m.startIndex && req.bytes <= m.endIndex
|
||||
: req.line >= m.startLine && req.line <= m.endLine
|
||||
&& (req.line > m.startLine || req.column >= m.startColumn)
|
||||
&& (req.line < m.endLine || req.column <= m.endColumn);
|
||||
}
|
||||
|
||||
RollbackAllocator rba;
|
||||
LexerConfig lexerConfig;
|
||||
lexerConfig.fileName = fileName;
|
||||
lexerConfig.stringBehavior = StringBehavior.source;
|
||||
auto tokens = getTokensForParser(usingStdin ? readStdin()
|
||||
: readFile(fileName), lexerConfig, cache);
|
||||
auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing));
|
||||
|
||||
auto messages = analyze(fileName, mod, config, moduleCache, tokens);
|
||||
|
||||
with (stdout.lockingTextWriter)
|
||||
{
|
||||
put("[");
|
||||
foreach (message; messages[].filter!matchesCursor)
|
||||
{
|
||||
resolveAutoFixes(message, fileName, moduleCache, tokens, mod, config);
|
||||
|
||||
foreach (i, autofix; message.autofixes)
|
||||
{
|
||||
put(i == 0 ? "\n" : ",\n");
|
||||
put("\t{\n");
|
||||
put(format!"\t\t\"name\": %s,\n"(JSONValue(autofix.name)));
|
||||
put("\t\t\"replacements\": [");
|
||||
foreach (j, replacement; autofix.expectReplacements)
|
||||
{
|
||||
put(j == 0 ? "\n" : ",\n");
|
||||
put(format!"\t\t\t{\"range\": [%d, %d], \"newText\": %s}"(
|
||||
replacement.range[0],
|
||||
replacement.range[1],
|
||||
JSONValue(replacement.newText)));
|
||||
}
|
||||
put("\n");
|
||||
put("\t\t]\n");
|
||||
put("\t}");
|
||||
}
|
||||
}
|
||||
put("\n]");
|
||||
}
|
||||
stdout.flush();
|
||||
}
|
||||
|
||||
private struct UserSelect
|
||||
{
|
||||
import std.string : strip;
|
||||
|
@ -736,82 +638,7 @@ bool shouldRun(check : BaseAnalyzer)(string moduleName, const ref StaticAnalysis
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a module is part of a user-specified include/exclude list.
|
||||
*
|
||||
* The user can specify a comma-separated list of filters, everyone needs to start with
|
||||
* either a '+' (inclusion) or '-' (exclusion).
|
||||
*
|
||||
* If no includes are specified, all modules are included.
|
||||
*/
|
||||
bool shouldRunDmd(check : BaseAnalyzerDmd)(const char[] moduleName, const ref StaticAnalysisConfig config)
|
||||
{
|
||||
enum string a = check.name;
|
||||
|
||||
if (mixin("config." ~ a) == Check.disabled)
|
||||
return false;
|
||||
|
||||
// By default, run the check
|
||||
if (!moduleName.length)
|
||||
return true;
|
||||
|
||||
auto filters = mixin("config.filters." ~ a);
|
||||
|
||||
// Check if there are filters are defined
|
||||
// filters starting with a comma are invalid
|
||||
if (filters.length == 0 || filters[0].length == 0)
|
||||
return true;
|
||||
|
||||
auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]);
|
||||
auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]);
|
||||
|
||||
// exclusion has preference over inclusion
|
||||
if (!excluders.empty && excluders.any!(s => moduleName.canFind(s)))
|
||||
return false;
|
||||
|
||||
if (!includers.empty)
|
||||
return includers.any!(s => moduleName.canFind(s));
|
||||
|
||||
// by default: include all modules
|
||||
return true;
|
||||
}
|
||||
|
||||
///
|
||||
unittest
|
||||
{
|
||||
bool test(string moduleName, string filters)
|
||||
{
|
||||
StaticAnalysisConfig config;
|
||||
// it doesn't matter which check we test here
|
||||
config.asm_style_check = Check.enabled;
|
||||
// this is done automatically by inifiled
|
||||
config.filters.asm_style_check = filters.split(",");
|
||||
return moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config);
|
||||
}
|
||||
|
||||
// test inclusion
|
||||
assert(test("std.foo", "+std."));
|
||||
// partial matches are ok
|
||||
assert(test("std.foo", "+bar,+foo"));
|
||||
// full as well
|
||||
assert(test("std.foo", "+bar,+std.foo,+foo"));
|
||||
// mismatch
|
||||
assert(!test("std.foo", "+bar,+banana"));
|
||||
|
||||
// test exclusion
|
||||
assert(!test("std.foo", "-std."));
|
||||
assert(!test("std.foo", "-bar,-std.foo"));
|
||||
assert(!test("std.foo", "-bar,-foo"));
|
||||
// mismatch
|
||||
assert(test("std.foo", "-bar,-banana"));
|
||||
|
||||
// test combination (exclusion has precedence)
|
||||
assert(!test("std.foo", "+foo,-foo"));
|
||||
assert(test("std.foo", "+foo,-bar"));
|
||||
assert(test("std.bar.foo", "-barr,+bar"));
|
||||
}
|
||||
|
||||
private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
|
||||
BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
|
||||
const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig, const Scope* moduleScope)
|
||||
{
|
||||
|
@ -862,6 +689,7 @@ MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig a
|
|||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
import dsymbol.symbol : DSymbol;
|
||||
import dscanner.analysis.autofix : resolveAutoFixFromCheck;
|
||||
|
||||
if (!staticAnalyze)
|
||||
return null;
|
||||
|
@ -900,231 +728,6 @@ MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig a
|
|||
return set;
|
||||
}
|
||||
|
||||
private void resolveAutoFixFromCheck(
|
||||
ref AutoFix autofix,
|
||||
BaseAnalyzer check,
|
||||
const Module m,
|
||||
scope const(Token)[] tokens,
|
||||
const AutoFixFormatting formattingConfig
|
||||
)
|
||||
{
|
||||
import std.sumtype : match;
|
||||
|
||||
autofix.replacements.match!(
|
||||
(AutoFix.ResolveContext context) {
|
||||
autofix.replacements = check.resolveAutoFix(m, tokens, context, formattingConfig);
|
||||
},
|
||||
(_) {}
|
||||
);
|
||||
}
|
||||
|
||||
void resolveAutoFixes(ref Message message, string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
resolveAutoFixes(message.checkName, message.autofixes, fileName, moduleCache,
|
||||
tokens, m, analysisConfig, overrideFormattingConfig);
|
||||
}
|
||||
|
||||
AutoFix.CodeReplacement[] resolveAutoFix(string messageCheckName, AutoFix.ResolveContext context,
|
||||
string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
AutoFix temp;
|
||||
temp.replacements = context;
|
||||
resolveAutoFixes(messageCheckName, (&temp)[0 .. 1], fileName, moduleCache,
|
||||
tokens, m, analysisConfig, overrideFormattingConfig);
|
||||
return temp.expectReplacements("resolving didn't work?!");
|
||||
}
|
||||
|
||||
void resolveAutoFixes(string messageCheckName, AutoFix[] autofixes, string fileName,
|
||||
ref ModuleCache moduleCache,
|
||||
scope const(Token)[] tokens, const Module m,
|
||||
const StaticAnalysisConfig analysisConfig,
|
||||
const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid)
|
||||
{
|
||||
import dsymbol.symbol : DSymbol;
|
||||
|
||||
const(AutoFixFormatting) formattingConfig =
|
||||
overrideFormattingConfig is AutoFixFormatting.invalid
|
||||
? analysisConfig.getAutoFixFormattingConfig()
|
||||
: overrideFormattingConfig;
|
||||
|
||||
scope first = new FirstPass(m, internString(fileName), &moduleCache, null);
|
||||
first.run();
|
||||
|
||||
secondPass(first.rootSymbol, first.moduleScope, moduleCache);
|
||||
auto moduleScope = first.moduleScope;
|
||||
scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol);
|
||||
scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol);
|
||||
scope(exit) typeid(Scope).destroy(first.moduleScope);
|
||||
|
||||
GC.disable;
|
||||
scope (exit)
|
||||
GC.enable;
|
||||
|
||||
foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope))
|
||||
{
|
||||
if (check.getName() == messageCheckName)
|
||||
{
|
||||
foreach (ref autofix; autofixes)
|
||||
autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Cannot find analyzer " ~ messageCheckName
|
||||
~ " to resolve autofix with.");
|
||||
}
|
||||
|
||||
void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements)
|
||||
{
|
||||
import std.ascii : isWhite;
|
||||
import std.string : strip;
|
||||
import std.utf : stride, strideBack;
|
||||
|
||||
enum WS
|
||||
{
|
||||
none, tab, space, newline
|
||||
}
|
||||
|
||||
WS getWS(size_t i)
|
||||
{
|
||||
if (cast(ptrdiff_t) i < 0 || i >= code.length)
|
||||
return WS.newline;
|
||||
switch (code[i])
|
||||
{
|
||||
case '\n':
|
||||
case '\r':
|
||||
return WS.newline;
|
||||
case '\t':
|
||||
return WS.tab;
|
||||
case ' ':
|
||||
return WS.space;
|
||||
default:
|
||||
return WS.none;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ref replacement; replacements)
|
||||
{
|
||||
assert(replacement.range[0] >= 0 && replacement.range[0] < code.length
|
||||
&& replacement.range[1] >= 0 && replacement.range[1] < code.length
|
||||
&& replacement.range[0] <= replacement.range[1], "trying to autofix whitespace on code that doesn't match with what the replacements were generated for");
|
||||
|
||||
void growRight()
|
||||
{
|
||||
// this is basically: replacement.range[1]++;
|
||||
if (code[replacement.range[1] .. $].startsWith("\r\n"))
|
||||
replacement.range[1] += 2;
|
||||
else if (replacement.range[1] < code.length)
|
||||
replacement.range[1] += code.stride(replacement.range[1]);
|
||||
}
|
||||
|
||||
void growLeft()
|
||||
{
|
||||
// this is basically: replacement.range[0]--;
|
||||
if (code[0 .. replacement.range[0]].endsWith("\r\n"))
|
||||
replacement.range[0] -= 2;
|
||||
else if (replacement.range[0] > 0)
|
||||
replacement.range[0] -= code.strideBack(replacement.range[0]);
|
||||
}
|
||||
|
||||
if (replacement.newText.strip.length)
|
||||
{
|
||||
if (replacement.newText.startsWith(" "))
|
||||
{
|
||||
// we insert with leading space, but there is a space/NL/SOF before
|
||||
// remove to-be-inserted space
|
||||
if (getWS(replacement.range[0] - 1))
|
||||
replacement.newText = replacement.newText[1 .. $];
|
||||
}
|
||||
if (replacement.newText.startsWith("]", ")"))
|
||||
{
|
||||
// when inserting `)`, consume regular space before
|
||||
if (getWS(replacement.range[0] - 1) == WS.space)
|
||||
growLeft();
|
||||
}
|
||||
if (replacement.newText.endsWith(" "))
|
||||
{
|
||||
// we insert with trailing space, but there is a space/NL/EOF after, chomp off
|
||||
if (getWS(replacement.range[1]))
|
||||
replacement.newText = replacement.newText[0 .. $ - 1];
|
||||
}
|
||||
if (replacement.newText.endsWith("[", "("))
|
||||
{
|
||||
if (getWS(replacement.range[1]))
|
||||
growRight();
|
||||
}
|
||||
}
|
||||
else if (!replacement.newText.length)
|
||||
{
|
||||
// after removing code and ending up with whitespace on both sides,
|
||||
// collapse 2 whitespace into one
|
||||
switch (getWS(replacement.range[1]))
|
||||
{
|
||||
case WS.newline:
|
||||
switch (getWS(replacement.range[0] - 1))
|
||||
{
|
||||
case WS.newline:
|
||||
// after removal we have NL ~ NL or SOF ~ NL,
|
||||
// remove right NL
|
||||
growRight();
|
||||
break;
|
||||
case WS.space:
|
||||
case WS.tab:
|
||||
// after removal we have space ~ NL,
|
||||
// remove the space
|
||||
growLeft();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case WS.space:
|
||||
case WS.tab:
|
||||
// for NL ~ space, SOF ~ space, space ~ space, tab ~ space,
|
||||
// for NL ~ tab, SOF ~ tab, space ~ tab, tab ~ tab
|
||||
// remove right space/tab
|
||||
if (getWS(replacement.range[0] - 1))
|
||||
growRight();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unittest
|
||||
{
|
||||
AutoFix.CodeReplacement r(int start, int end, string s)
|
||||
{
|
||||
return AutoFix.CodeReplacement([start, end], s);
|
||||
}
|
||||
|
||||
string test(string code, AutoFix.CodeReplacement[] replacements...)
|
||||
{
|
||||
replacements.sort!"a.range[0] < b.range[0]";
|
||||
improveAutoFixWhitespace(code, replacements);
|
||||
foreach_reverse (r; replacements)
|
||||
code = code[0 .. r.range[0]] ~ r.newText ~ code[r.range[1] .. $];
|
||||
return code;
|
||||
}
|
||||
|
||||
assert(test("import a;\nimport b;", r(0, 9, "")) == "import b;");
|
||||
assert(test("import a;\r\nimport b;", r(0, 9, "")) == "import b;");
|
||||
assert(test("import a;\nimport b;", r(8, 9, "")) == "import a\nimport b;");
|
||||
assert(test("import a;\nimport b;", r(7, 8, "")) == "import ;\nimport b;");
|
||||
assert(test("import a;\r\nimport b;", r(7, 8, "")) == "import ;\r\nimport b;");
|
||||
assert(test("a b c", r(2, 3, "")) == "a c");
|
||||
}
|
||||
|
||||
version (unittest)
|
||||
{
|
||||
shared static this()
|
||||
|
@ -1142,237 +745,3 @@ version (unittest)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleName, const StaticAnalysisConfig config)
|
||||
{
|
||||
MessageSet set = new MessageSet;
|
||||
BaseAnalyzerDmd[] visitors;
|
||||
|
||||
if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config))
|
||||
visitors ~= new ObjectConstCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(EnumArrayVisitor!ASTCodegen)(config))
|
||||
visitors ~= new EnumArrayVisitor!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(DeleteCheck!ASTCodegen)(config))
|
||||
visitors ~= new DeleteCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(FinalAttributeChecker!ASTCodegen)(config))
|
||||
visitors ~= new FinalAttributeChecker!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ImportSortednessCheck!ASTCodegen)(config))
|
||||
visitors ~= new ImportSortednessCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(IncorrectInfiniteRangeCheck!ASTCodegen)(config))
|
||||
visitors ~= new IncorrectInfiniteRangeCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantAttributesCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantAttributesCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LengthSubtractionCheck!ASTCodegen)(config))
|
||||
visitors ~= new LengthSubtractionCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AliasSyntaxCheck!ASTCodegen)(config))
|
||||
visitors ~= new AliasSyntaxCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ExplicitlyAnnotatedUnittestCheck!ASTCodegen)(config))
|
||||
visitors ~= new ExplicitlyAnnotatedUnittestCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ConstructorCheck!ASTCodegen)(config))
|
||||
visitors ~= new ConstructorCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AssertWithoutMessageCheck!ASTCodegen)(config))
|
||||
visitors ~= new AssertWithoutMessageCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.assert_without_msg == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LocalImportCheck!ASTCodegen)(config))
|
||||
visitors ~= new LocalImportCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(OpEqualsWithoutToHashCheck!ASTCodegen)(config))
|
||||
visitors ~= new OpEqualsWithoutToHashCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.opequals_tohash_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(TrustTooMuchCheck!ASTCodegen)(config))
|
||||
visitors ~= new TrustTooMuchCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.trust_too_much == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AutoRefAssignmentCheck!ASTCodegen)(config))
|
||||
visitors ~= new AutoRefAssignmentCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LogicPrecedenceCheck!ASTCodegen)(config))
|
||||
visitors ~= new LogicPrecedenceCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.logical_precedence_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedLabelCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedLabelCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_label_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BuiltinPropertyNameCheck!ASTCodegen)(config))
|
||||
visitors ~= new BuiltinPropertyNameCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(PokemonExceptionCheck!ASTCodegen)(config))
|
||||
visitors ~= new PokemonExceptionCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.exception_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BackwardsRangeCheck!ASTCodegen)(config))
|
||||
visitors ~= new BackwardsRangeCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.backwards_range_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ProperlyDocumentedPublicFunctions!ASTCodegen)(config))
|
||||
visitors ~= new ProperlyDocumentedPublicFunctions!ASTCodegen(
|
||||
fileName,
|
||||
config.properly_documented_public_functions == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantParenCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantParenCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.redundant_parens_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(StaticIfElse!ASTCodegen)(config))
|
||||
visitors ~= new StaticIfElse!ASTCodegen(
|
||||
fileName,
|
||||
config.static_if_else_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UselessAssertCheck!ASTCodegen)(config))
|
||||
visitors ~= new UselessAssertCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.useless_assert_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config))
|
||||
visitors ~= new AsmStyleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.asm_style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantStorageClassCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantStorageClassCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.redundant_storage_classes == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(NumberStyleCheck!ASTCodegen)(config))
|
||||
visitors ~= new NumberStyleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.number_style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(IfElseSameCheck!ASTCodegen)(config))
|
||||
visitors ~= new IfElseSameCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.if_else_same_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(CyclomaticComplexityCheck!ASTCodegen)(config))
|
||||
visitors ~= new CyclomaticComplexityCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.cyclomatic_complexity == Check.skipTests && !ut,
|
||||
config.max_cyclomatic_complexity.to!int
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LabelVarNameCheck!ASTCodegen)(config))
|
||||
visitors ~= new LabelVarNameCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.label_var_same_name_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LambdaReturnCheck!ASTCodegen)(config))
|
||||
visitors ~= new LambdaReturnCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.lambda_return_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AlwaysCurlyCheck!ASTCodegen)(config))
|
||||
visitors ~= new AlwaysCurlyCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.always_curly_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(StyleChecker!ASTCodegen)(config))
|
||||
visitors ~= new StyleChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AutoFunctionChecker!ASTCodegen)(config))
|
||||
visitors ~= new AutoFunctionChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.auto_function_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedParameterCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedParameterCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_parameter_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedVariableCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedVariableCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_variable_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnmodifiedFinder!ASTCodegen)(config))
|
||||
visitors ~= new UnmodifiedFinder!ASTCodegen(
|
||||
fileName,
|
||||
config.could_be_immutable_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BodyOnDisabledFuncsCheck!ASTCodegen)(config))
|
||||
visitors ~= new BodyOnDisabledFuncsCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.body_on_disabled_func_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UselessInitializerChecker!ASTCodegen)(config))
|
||||
visitors ~= new UselessInitializerChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.useless_initializer == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(HasPublicExampleCheck!ASTCodegen)(config))
|
||||
visitors ~= new HasPublicExampleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.has_public_example == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!LineLengthCheck(config))
|
||||
visitors ~= new LineLengthCheck(
|
||||
fileName,
|
||||
config.long_line_check == Check.skipTests && !ut,
|
||||
config.max_line_length
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedResultChecker!ASTCodegen)(config))
|
||||
visitors ~= new UnusedResultChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_result == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
foreach (visitor; visitors)
|
||||
{
|
||||
m.accept(visitor);
|
||||
|
||||
foreach (message; visitor.messages)
|
||||
set.insert(message);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
|
399
src/dscanner/analysis/rundmd.d
Normal file
399
src/dscanner/analysis/rundmd.d
Normal file
|
@ -0,0 +1,399 @@
|
|||
module dscanner.analysis.rundmd;
|
||||
|
||||
import std.algorithm : any, canFind, filter, map;
|
||||
import std.conv : to;
|
||||
|
||||
import dmd.astcodegen;
|
||||
import dmd.dmodule : Module;
|
||||
import dmd.frontend;
|
||||
|
||||
import dscanner.analysis.config : Check, StaticAnalysisConfig;
|
||||
import dscanner.analysis.base : BaseAnalyzerDmd, MessageSet;
|
||||
|
||||
import dscanner.analysis.alias_syntax_check : AliasSyntaxCheck;
|
||||
import dscanner.analysis.always_curly : AlwaysCurlyCheck;
|
||||
import dscanner.analysis.asm_style : AsmStyleCheck;
|
||||
import dscanner.analysis.assert_without_msg : AssertWithoutMessageCheck;
|
||||
import dscanner.analysis.auto_function : AutoFunctionChecker;
|
||||
import dscanner.analysis.auto_ref_assignment : AutoRefAssignmentCheck;
|
||||
import dscanner.analysis.body_on_disabled_funcs : BodyOnDisabledFuncsCheck;
|
||||
import dscanner.analysis.builtin_property_names : BuiltinPropertyNameCheck;
|
||||
import dscanner.analysis.constructors : ConstructorCheck;
|
||||
import dscanner.analysis.cyclomatic_complexity : CyclomaticComplexityCheck;
|
||||
import dscanner.analysis.del : DeleteCheck;
|
||||
import dscanner.analysis.enumarrayliteral : EnumArrayVisitor;
|
||||
import dscanner.analysis.explicitly_annotated_unittests : ExplicitlyAnnotatedUnittestCheck;
|
||||
import dscanner.analysis.final_attribute : FinalAttributeChecker;
|
||||
import dscanner.analysis.has_public_example : HasPublicExampleCheck;
|
||||
import dscanner.analysis.ifelsesame : IfElseSameCheck;
|
||||
import dscanner.analysis.imports_sortedness : ImportSortednessCheck;
|
||||
import dscanner.analysis.incorrect_infinite_range : IncorrectInfiniteRangeCheck;
|
||||
import dscanner.analysis.label_var_same_name_check : LabelVarNameCheck;
|
||||
import dscanner.analysis.lambda_return_check : LambdaReturnCheck;
|
||||
import dscanner.analysis.length_subtraction : LengthSubtractionCheck;
|
||||
import dscanner.analysis.line_length : LineLengthCheck;
|
||||
import dscanner.analysis.local_imports : LocalImportCheck;
|
||||
import dscanner.analysis.logic_precedence : LogicPrecedenceCheck;
|
||||
import dscanner.analysis.numbers : NumberStyleCheck;
|
||||
import dscanner.analysis.objectconst : ObjectConstCheck;
|
||||
import dscanner.analysis.opequals_without_tohash : OpEqualsWithoutToHashCheck;
|
||||
import dscanner.analysis.pokemon : PokemonExceptionCheck;
|
||||
import dscanner.analysis.properly_documented_public_functions : ProperlyDocumentedPublicFunctions;
|
||||
import dscanner.analysis.range : BackwardsRangeCheck;
|
||||
import dscanner.analysis.redundant_attributes : RedundantAttributesCheck;
|
||||
import dscanner.analysis.redundant_parens : RedundantParenCheck;
|
||||
import dscanner.analysis.redundant_storage_class : RedundantStorageClassCheck;
|
||||
import dscanner.analysis.static_if_else : StaticIfElse;
|
||||
import dscanner.analysis.style : StyleChecker;
|
||||
import dscanner.analysis.trust_too_much : TrustTooMuchCheck;
|
||||
import dscanner.analysis.unmodified : UnmodifiedFinder;
|
||||
import dscanner.analysis.unused_label : UnusedLabelCheck;
|
||||
import dscanner.analysis.unused_parameter : UnusedParameterCheck;
|
||||
import dscanner.analysis.unused_result : UnusedResultChecker;
|
||||
import dscanner.analysis.unused_variable : UnusedVariableCheck;
|
||||
import dscanner.analysis.useless_assert : UselessAssertCheck;
|
||||
import dscanner.analysis.useless_initializer : UselessInitializerChecker;
|
||||
|
||||
version (unittest)
|
||||
enum ut = true;
|
||||
else
|
||||
enum ut = false;
|
||||
|
||||
Module parseDmdModule(string fileName, string sourceCode)
|
||||
{
|
||||
setupDmd();
|
||||
|
||||
auto code = sourceCode;
|
||||
if (code[$ - 1] != '\0')
|
||||
code ~= '\0';
|
||||
|
||||
auto dmdModule = dmd.frontend.parseModule(cast(const(char)[]) fileName, cast(const (char)[]) code);
|
||||
return dmdModule.module_;
|
||||
}
|
||||
|
||||
private void setupDmd()
|
||||
{
|
||||
import std.path : dirName;
|
||||
import dmd.arraytypes : Strings;
|
||||
import dmd.globals : global;
|
||||
|
||||
auto dmdParentDir = dirName(dirName(dirName(dirName(__FILE_FULL_PATH__))));
|
||||
auto dmdDirPath = dmdParentDir ~ "/dmd" ~ "\0";
|
||||
auto druntimeDirPath = dmdParentDir ~ "/dmd/druntime/src" ~ "\0";
|
||||
global.params.useUnitTests = true;
|
||||
global.path = Strings();
|
||||
global.path.push(dmdDirPath.ptr);
|
||||
global.path.push(druntimeDirPath.ptr);
|
||||
initDMD();
|
||||
}
|
||||
|
||||
MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleName, const StaticAnalysisConfig config)
|
||||
{
|
||||
MessageSet set = new MessageSet;
|
||||
BaseAnalyzerDmd[] visitors;
|
||||
|
||||
if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config))
|
||||
visitors ~= new ObjectConstCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(EnumArrayVisitor!ASTCodegen)(config))
|
||||
visitors ~= new EnumArrayVisitor!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(DeleteCheck!ASTCodegen)(config))
|
||||
visitors ~= new DeleteCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(FinalAttributeChecker!ASTCodegen)(config))
|
||||
visitors ~= new FinalAttributeChecker!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ImportSortednessCheck!ASTCodegen)(config))
|
||||
visitors ~= new ImportSortednessCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(IncorrectInfiniteRangeCheck!ASTCodegen)(config))
|
||||
visitors ~= new IncorrectInfiniteRangeCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantAttributesCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantAttributesCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LengthSubtractionCheck!ASTCodegen)(config))
|
||||
visitors ~= new LengthSubtractionCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AliasSyntaxCheck!ASTCodegen)(config))
|
||||
visitors ~= new AliasSyntaxCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ExplicitlyAnnotatedUnittestCheck!ASTCodegen)(config))
|
||||
visitors ~= new ExplicitlyAnnotatedUnittestCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ConstructorCheck!ASTCodegen)(config))
|
||||
visitors ~= new ConstructorCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AssertWithoutMessageCheck!ASTCodegen)(config))
|
||||
visitors ~= new AssertWithoutMessageCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.assert_without_msg == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LocalImportCheck!ASTCodegen)(config))
|
||||
visitors ~= new LocalImportCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(OpEqualsWithoutToHashCheck!ASTCodegen)(config))
|
||||
visitors ~= new OpEqualsWithoutToHashCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.opequals_tohash_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(TrustTooMuchCheck!ASTCodegen)(config))
|
||||
visitors ~= new TrustTooMuchCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.trust_too_much == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AutoRefAssignmentCheck!ASTCodegen)(config))
|
||||
visitors ~= new AutoRefAssignmentCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LogicPrecedenceCheck!ASTCodegen)(config))
|
||||
visitors ~= new LogicPrecedenceCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.logical_precedence_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedLabelCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedLabelCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_label_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BuiltinPropertyNameCheck!ASTCodegen)(config))
|
||||
visitors ~= new BuiltinPropertyNameCheck!ASTCodegen(fileName);
|
||||
|
||||
if (moduleName.shouldRunDmd!(PokemonExceptionCheck!ASTCodegen)(config))
|
||||
visitors ~= new PokemonExceptionCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.exception_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BackwardsRangeCheck!ASTCodegen)(config))
|
||||
visitors ~= new BackwardsRangeCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.backwards_range_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(ProperlyDocumentedPublicFunctions!ASTCodegen)(config))
|
||||
visitors ~= new ProperlyDocumentedPublicFunctions!ASTCodegen(
|
||||
fileName,
|
||||
config.properly_documented_public_functions == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantParenCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantParenCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.redundant_parens_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(StaticIfElse!ASTCodegen)(config))
|
||||
visitors ~= new StaticIfElse!ASTCodegen(
|
||||
fileName,
|
||||
config.static_if_else_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UselessAssertCheck!ASTCodegen)(config))
|
||||
visitors ~= new UselessAssertCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.useless_assert_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config))
|
||||
visitors ~= new AsmStyleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.asm_style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(RedundantStorageClassCheck!ASTCodegen)(config))
|
||||
visitors ~= new RedundantStorageClassCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.redundant_storage_classes == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(NumberStyleCheck!ASTCodegen)(config))
|
||||
visitors ~= new NumberStyleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.number_style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(IfElseSameCheck!ASTCodegen)(config))
|
||||
visitors ~= new IfElseSameCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.if_else_same_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(CyclomaticComplexityCheck!ASTCodegen)(config))
|
||||
visitors ~= new CyclomaticComplexityCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.cyclomatic_complexity == Check.skipTests && !ut,
|
||||
config.max_cyclomatic_complexity.to!int
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LabelVarNameCheck!ASTCodegen)(config))
|
||||
visitors ~= new LabelVarNameCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.label_var_same_name_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(LambdaReturnCheck!ASTCodegen)(config))
|
||||
visitors ~= new LambdaReturnCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.lambda_return_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AlwaysCurlyCheck!ASTCodegen)(config))
|
||||
visitors ~= new AlwaysCurlyCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.always_curly_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(StyleChecker!ASTCodegen)(config))
|
||||
visitors ~= new StyleChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.style_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(AutoFunctionChecker!ASTCodegen)(config))
|
||||
visitors ~= new AutoFunctionChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.auto_function_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedParameterCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedParameterCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_parameter_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedVariableCheck!ASTCodegen)(config))
|
||||
visitors ~= new UnusedVariableCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_variable_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnmodifiedFinder!ASTCodegen)(config))
|
||||
visitors ~= new UnmodifiedFinder!ASTCodegen(
|
||||
fileName,
|
||||
config.could_be_immutable_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(BodyOnDisabledFuncsCheck!ASTCodegen)(config))
|
||||
visitors ~= new BodyOnDisabledFuncsCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.body_on_disabled_func_check == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UselessInitializerChecker!ASTCodegen)(config))
|
||||
visitors ~= new UselessInitializerChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.useless_initializer == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(HasPublicExampleCheck!ASTCodegen)(config))
|
||||
visitors ~= new HasPublicExampleCheck!ASTCodegen(
|
||||
fileName,
|
||||
config.has_public_example == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!LineLengthCheck(config))
|
||||
visitors ~= new LineLengthCheck(
|
||||
fileName,
|
||||
config.long_line_check == Check.skipTests && !ut,
|
||||
config.max_line_length
|
||||
);
|
||||
|
||||
if (moduleName.shouldRunDmd!(UnusedResultChecker!ASTCodegen)(config))
|
||||
visitors ~= new UnusedResultChecker!ASTCodegen(
|
||||
fileName,
|
||||
config.unused_result == Check.skipTests && !ut
|
||||
);
|
||||
|
||||
foreach (visitor; visitors)
|
||||
{
|
||||
m.accept(visitor);
|
||||
|
||||
foreach (message; visitor.messages)
|
||||
set.insert(message);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a module is part of a user-specified include/exclude list.
|
||||
*
|
||||
* The user can specify a comma-separated list of filters, everyone needs to start with
|
||||
* either a '+' (inclusion) or '-' (exclusion).
|
||||
*
|
||||
* If no includes are specified, all modules are included.
|
||||
*/
|
||||
private bool shouldRunDmd(check : BaseAnalyzerDmd)(const char[] moduleName, const ref StaticAnalysisConfig config)
|
||||
{
|
||||
enum string a = check.name;
|
||||
|
||||
if (mixin("config." ~ a) == Check.disabled)
|
||||
return false;
|
||||
|
||||
// By default, run the check
|
||||
if (!moduleName.length)
|
||||
return true;
|
||||
|
||||
auto filters = mixin("config.filters." ~ a);
|
||||
|
||||
// Check if there are filters are defined
|
||||
// filters starting with a comma are invalid
|
||||
if (filters.length == 0 || filters[0].length == 0)
|
||||
return true;
|
||||
|
||||
auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]);
|
||||
auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]);
|
||||
|
||||
// exclusion has preference over inclusion
|
||||
if (!excluders.empty && excluders.any!(s => moduleName.canFind(s)))
|
||||
return false;
|
||||
|
||||
if (!includers.empty)
|
||||
return includers.any!(s => moduleName.canFind(s));
|
||||
|
||||
// by default: include all modules
|
||||
return true;
|
||||
}
|
||||
|
||||
///
|
||||
unittest
|
||||
{
|
||||
bool test(string moduleName, string filters)
|
||||
{
|
||||
import std.array : split;
|
||||
|
||||
StaticAnalysisConfig config;
|
||||
// it doesn't matter which check we test here
|
||||
config.asm_style_check = Check.enabled;
|
||||
// this is done automatically by inifiled
|
||||
config.filters.asm_style_check = filters.split(",");
|
||||
return moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config);
|
||||
}
|
||||
|
||||
// test inclusion
|
||||
assert(test("std.foo", "+std."));
|
||||
// partial matches are ok
|
||||
assert(test("std.foo", "+bar,+foo"));
|
||||
// full as well
|
||||
assert(test("std.foo", "+bar,+std.foo,+foo"));
|
||||
// mismatch
|
||||
assert(!test("std.foo", "+bar,+banana"));
|
||||
|
||||
// test exclusion
|
||||
assert(!test("std.foo", "-std."));
|
||||
assert(!test("std.foo", "-bar,-std.foo"));
|
||||
assert(!test("std.foo", "-bar,-foo"));
|
||||
// mismatch
|
||||
assert(test("std.foo", "-bar,-banana"));
|
||||
|
||||
// test combination (exclusion has precedence)
|
||||
assert(!test("std.foo", "+foo,-foo"));
|
||||
assert(test("std.foo", "+foo,-bar"));
|
||||
assert(test("std.bar.foo", "-barr,+bar"));
|
||||
}
|
|
@ -1,461 +0,0 @@
|
|||
// Copyright Brian Schott (Hackerpilot) 2014-2015.
|
||||
// Distributed under the Boost Software License, Version 1.0.
|
||||
// (See accompanying file LICENSE_1_0.txt or copy at
|
||||
// http://www.boost.org/LICENSE_1_0.txt)
|
||||
module dscanner.analysis.unused;
|
||||
|
||||
import dparse.ast;
|
||||
import dparse.lexer;
|
||||
import dscanner.analysis.base;
|
||||
import std.container;
|
||||
import std.regex : Regex, regex, matchAll;
|
||||
import dsymbol.scope_ : Scope;
|
||||
import std.algorithm : all;
|
||||
|
||||
/**
|
||||
* Checks for unused variables.
|
||||
*/
|
||||
abstract class UnusedIdentifierCheck : BaseAnalyzer
|
||||
{
|
||||
alias visit = BaseAnalyzer.visit;
|
||||
|
||||
/**
|
||||
*/
|
||||
this(BaseAnalyzerArguments args)
|
||||
{
|
||||
super(args);
|
||||
re = regex("[\\p{Alphabetic}_][\\w_]*");
|
||||
}
|
||||
|
||||
override void visit(const Module mod)
|
||||
{
|
||||
pushScope();
|
||||
mod.accept(this);
|
||||
popScope();
|
||||
}
|
||||
|
||||
override void visit(const Declaration declaration)
|
||||
{
|
||||
if (!isOverride)
|
||||
foreach (attribute; declaration.attributes)
|
||||
isOverride = isOverride || (attribute.attribute == tok!"override");
|
||||
declaration.accept(this);
|
||||
isOverride = false;
|
||||
}
|
||||
|
||||
override void visit(const FunctionDeclaration functionDec)
|
||||
{
|
||||
pushScope();
|
||||
if (functionDec.functionBody
|
||||
&& (functionDec.functionBody.specifiedFunctionBody
|
||||
|| functionDec.functionBody.shortenedFunctionBody))
|
||||
{
|
||||
immutable bool ias = inAggregateScope;
|
||||
inAggregateScope = false;
|
||||
if (!isOverride)
|
||||
functionDec.parameters.accept(this);
|
||||
functionDec.functionBody.accept(this);
|
||||
inAggregateScope = ias;
|
||||
}
|
||||
popScope();
|
||||
}
|
||||
|
||||
mixin PartsUseVariables!AliasInitializer;
|
||||
mixin PartsUseVariables!ArgumentList;
|
||||
mixin PartsUseVariables!AssertExpression;
|
||||
mixin PartsUseVariables!ClassDeclaration;
|
||||
mixin PartsUseVariables!FunctionBody;
|
||||
mixin PartsUseVariables!FunctionCallExpression;
|
||||
mixin PartsUseVariables!FunctionDeclaration;
|
||||
mixin PartsUseVariables!IndexExpression;
|
||||
mixin PartsUseVariables!Initializer;
|
||||
mixin PartsUseVariables!InterfaceDeclaration;
|
||||
mixin PartsUseVariables!NewExpression;
|
||||
mixin PartsUseVariables!StaticIfCondition;
|
||||
mixin PartsUseVariables!StructDeclaration;
|
||||
mixin PartsUseVariables!TemplateArgumentList;
|
||||
mixin PartsUseVariables!ThrowExpression;
|
||||
mixin PartsUseVariables!CastExpression;
|
||||
|
||||
override void dynamicDispatch(const ExpressionNode n)
|
||||
{
|
||||
interestDepth++;
|
||||
super.dynamicDispatch(n);
|
||||
interestDepth--;
|
||||
}
|
||||
|
||||
override void visit(const SwitchStatement switchStatement)
|
||||
{
|
||||
if (switchStatement.expression !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
switchStatement.expression.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
switchStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const WhileStatement whileStatement)
|
||||
{
|
||||
if (whileStatement.condition.expression !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
whileStatement.condition.expression.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (whileStatement.declarationOrStatement !is null)
|
||||
whileStatement.declarationOrStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const DoStatement doStatement)
|
||||
{
|
||||
if (doStatement.expression !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
doStatement.expression.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (doStatement.statementNoCaseNoDefault !is null)
|
||||
doStatement.statementNoCaseNoDefault.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const ForStatement forStatement)
|
||||
{
|
||||
if (forStatement.initialization !is null)
|
||||
forStatement.initialization.accept(this);
|
||||
if (forStatement.test !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
forStatement.test.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (forStatement.increment !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
forStatement.increment.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (forStatement.declarationOrStatement !is null)
|
||||
forStatement.declarationOrStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const IfStatement ifStatement)
|
||||
{
|
||||
if (ifStatement.condition.expression !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
ifStatement.condition.expression.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (ifStatement.thenStatement !is null)
|
||||
ifStatement.thenStatement.accept(this);
|
||||
if (ifStatement.elseStatement !is null)
|
||||
ifStatement.elseStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const ForeachStatement foreachStatement)
|
||||
{
|
||||
if (foreachStatement.low !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
foreachStatement.low.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
if (foreachStatement.high !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
foreachStatement.high.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
foreachStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const AssignExpression assignExp)
|
||||
{
|
||||
interestDepth++;
|
||||
assignExp.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
|
||||
override void visit(const TemplateDeclaration templateDeclaration)
|
||||
{
|
||||
immutable inAgg = inAggregateScope;
|
||||
inAggregateScope = true;
|
||||
templateDeclaration.accept(this);
|
||||
inAggregateScope = inAgg;
|
||||
}
|
||||
|
||||
override void visit(const IdentifierOrTemplateChain chain)
|
||||
{
|
||||
if (interestDepth > 0 && chain.identifiersOrTemplateInstances[0].identifier != tok!"")
|
||||
variableUsed(chain.identifiersOrTemplateInstances[0].identifier.text);
|
||||
chain.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const TemplateSingleArgument single)
|
||||
{
|
||||
if (single.token != tok!"")
|
||||
variableUsed(single.token.text);
|
||||
}
|
||||
|
||||
override void visit(const UnaryExpression unary)
|
||||
{
|
||||
const bool interesting = unary.prefix == tok!"*" || unary.unaryExpression !is null;
|
||||
interestDepth += interesting;
|
||||
unary.accept(this);
|
||||
interestDepth -= interesting;
|
||||
}
|
||||
|
||||
override void visit(const MixinExpression mix)
|
||||
{
|
||||
interestDepth++;
|
||||
mixinDepth++;
|
||||
mix.accept(this);
|
||||
mixinDepth--;
|
||||
interestDepth--;
|
||||
}
|
||||
|
||||
override void visit(const PrimaryExpression primary)
|
||||
{
|
||||
if (interestDepth > 0)
|
||||
{
|
||||
const IdentifierOrTemplateInstance idt = primary.identifierOrTemplateInstance;
|
||||
|
||||
if (idt !is null)
|
||||
{
|
||||
if (idt.identifier != tok!"")
|
||||
variableUsed(idt.identifier.text);
|
||||
else if (idt.templateInstance && idt.templateInstance.identifier != tok!"")
|
||||
variableUsed(idt.templateInstance.identifier.text);
|
||||
}
|
||||
if ((mixinDepth > 0 && primary.primary == tok!"stringLiteral")
|
||||
|| primary.primary == tok!"wstringLiteral"
|
||||
|| primary.primary == tok!"dstringLiteral")
|
||||
{
|
||||
foreach (part; matchAll(primary.primary.text, re))
|
||||
{
|
||||
void checkTree(in size_t treeIndex)
|
||||
{
|
||||
auto uu = UnUsed(part.hit);
|
||||
auto r = tree[treeIndex].equalRange(&uu);
|
||||
if (!r.empty)
|
||||
r.front.uncertain = true;
|
||||
}
|
||||
checkTree(tree.length - 1);
|
||||
if (tree.length >= 2)
|
||||
checkTree(tree.length - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
primary.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const ReturnStatement retStatement)
|
||||
{
|
||||
if (retStatement.expression !is null)
|
||||
{
|
||||
interestDepth++;
|
||||
visit(retStatement.expression);
|
||||
interestDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
override void visit(const BlockStatement blockStatement)
|
||||
{
|
||||
immutable bool sb = inAggregateScope;
|
||||
inAggregateScope = false;
|
||||
if (blockStatementIntroducesScope)
|
||||
pushScope();
|
||||
blockStatement.accept(this);
|
||||
if (blockStatementIntroducesScope)
|
||||
popScope();
|
||||
inAggregateScope = sb;
|
||||
}
|
||||
|
||||
override void visit(const Type2 tp)
|
||||
{
|
||||
if (tp.typeIdentifierPart &&
|
||||
tp.typeIdentifierPart.identifierOrTemplateInstance)
|
||||
{
|
||||
const IdentifierOrTemplateInstance idt = tp.typeIdentifierPart.identifierOrTemplateInstance;
|
||||
if (idt.identifier != tok!"")
|
||||
variableUsed(idt.identifier.text);
|
||||
else if (idt.templateInstance)
|
||||
{
|
||||
const TemplateInstance ti = idt.templateInstance;
|
||||
if (ti.identifier != tok!"")
|
||||
variableUsed(idt.templateInstance.identifier.text);
|
||||
if (ti.templateArguments && ti.templateArguments.templateSingleArgument)
|
||||
variableUsed(ti.templateArguments.templateSingleArgument.token.text);
|
||||
}
|
||||
}
|
||||
tp.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const WithStatement withStatetement)
|
||||
{
|
||||
interestDepth++;
|
||||
if (withStatetement.expression)
|
||||
withStatetement.expression.accept(this);
|
||||
interestDepth--;
|
||||
if (withStatetement.declarationOrStatement)
|
||||
withStatetement.declarationOrStatement.accept(this);
|
||||
}
|
||||
|
||||
override void visit(const StructBody structBody)
|
||||
{
|
||||
immutable bool sb = inAggregateScope;
|
||||
inAggregateScope = true;
|
||||
foreach (dec; structBody.declarations)
|
||||
visit(dec);
|
||||
inAggregateScope = sb;
|
||||
}
|
||||
|
||||
override void visit(const ConditionalStatement conditionalStatement)
|
||||
{
|
||||
immutable bool cs = blockStatementIntroducesScope;
|
||||
blockStatementIntroducesScope = false;
|
||||
conditionalStatement.accept(this);
|
||||
blockStatementIntroducesScope = cs;
|
||||
}
|
||||
|
||||
override void visit(const AsmPrimaryExp primary)
|
||||
{
|
||||
if (primary.token != tok!"")
|
||||
variableUsed(primary.token.text);
|
||||
if (primary.identifierChain !is null)
|
||||
variableUsed(primary.identifierChain.identifiers[0].text);
|
||||
}
|
||||
|
||||
override void visit(const TraitsExpression)
|
||||
{
|
||||
// issue #266: Ignore unused variables inside of `__traits` expressions
|
||||
}
|
||||
|
||||
override void visit(const TypeofExpression)
|
||||
{
|
||||
// issue #270: Ignore unused variables inside of `typeof` expressions
|
||||
}
|
||||
|
||||
abstract protected void popScope();
|
||||
|
||||
protected uint interestDepth;
|
||||
|
||||
protected Tree[] tree;
|
||||
|
||||
protected void variableDeclared(string name, Token token, bool isRef)
|
||||
{
|
||||
if (inAggregateScope || name.all!(a => a == '_'))
|
||||
return;
|
||||
tree[$ - 1].insert(new UnUsed(name, token, isRef));
|
||||
}
|
||||
|
||||
protected void pushScope()
|
||||
{
|
||||
tree ~= new Tree;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
struct UnUsed
|
||||
{
|
||||
string name;
|
||||
Token token;
|
||||
bool isRef;
|
||||
bool uncertain;
|
||||
}
|
||||
|
||||
alias Tree = RedBlackTree!(UnUsed*, "a.name < b.name");
|
||||
|
||||
mixin template PartsUseVariables(NodeType)
|
||||
{
|
||||
override void visit(const NodeType node)
|
||||
{
|
||||
interestDepth++;
|
||||
node.accept(this);
|
||||
interestDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
void variableUsed(string name)
|
||||
{
|
||||
size_t treeIndex = tree.length - 1;
|
||||
auto uu = UnUsed(name);
|
||||
while (true)
|
||||
{
|
||||
if (tree[treeIndex].removeKey(&uu) != 0 || treeIndex == 0)
|
||||
break;
|
||||
treeIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
Regex!char re;
|
||||
|
||||
bool inAggregateScope;
|
||||
|
||||
uint mixinDepth;
|
||||
|
||||
bool isOverride;
|
||||
|
||||
bool blockStatementIntroducesScope = true;
|
||||
}
|
||||
|
||||
/// Base class for unused parameter/variables checks
|
||||
abstract class UnusedStorageCheck : UnusedIdentifierCheck
|
||||
{
|
||||
alias visit = UnusedIdentifierCheck.visit;
|
||||
|
||||
/**
|
||||
Ignore declarations which are allowed to be unused, e.g. inside of a
|
||||
speculative compilation: __traits(compiles, { S s = 0; })
|
||||
**/
|
||||
uint ignoreDeclarations = 0;
|
||||
|
||||
/// Kind of declaration for error messages e.g. "Variable"
|
||||
const string publicType;
|
||||
|
||||
/// Kind of declaration for error reports e.g. "unused_variable"
|
||||
const string reportType;
|
||||
|
||||
/**
|
||||
* Params:
|
||||
* args = commonly shared analyzer arguments
|
||||
* publicType = declaration kind used in error messages, e.g. "Variable"s
|
||||
* reportType = declaration kind used in error reports, e.g. "unused_variable"
|
||||
*/
|
||||
this(BaseAnalyzerArguments args, string publicType = null, string reportType = null)
|
||||
{
|
||||
super(args);
|
||||
this.publicType = publicType;
|
||||
this.reportType = reportType;
|
||||
}
|
||||
|
||||
override void visit(const TraitsExpression traitsExp)
|
||||
{
|
||||
// issue #788: Enum values might be used inside of `__traits` expressions, e.g.:
|
||||
// enum name = "abc";
|
||||
// __traits(hasMember, S, name);
|
||||
ignoreDeclarations++;
|
||||
if (traitsExp.templateArgumentList)
|
||||
traitsExp.templateArgumentList.accept(this);
|
||||
ignoreDeclarations--;
|
||||
}
|
||||
|
||||
override final protected void popScope()
|
||||
{
|
||||
if (!ignoreDeclarations)
|
||||
{
|
||||
foreach (uu; tree[$ - 1])
|
||||
{
|
||||
if (!uu.isRef && tree.length > 1)
|
||||
{
|
||||
if (uu.uncertain)
|
||||
continue;
|
||||
immutable string errorMessage = publicType ~ ' ' ~ uu.name ~ " is never used.";
|
||||
addErrorMessage(uu.token, "dscanner.suspicious." ~ reportType, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
tree = tree[0 .. $ - 1];
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import dscanner.outliner;
|
|||
import dscanner.symbol_finder;
|
||||
import dscanner.analysis.run;
|
||||
import dscanner.analysis.config;
|
||||
import dscanner.analysis.autofix : listAutofixes;
|
||||
import dscanner.dscanner_version;
|
||||
import dscanner.utils;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue