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:
Vladiwostok 2024-09-25 12:39:00 +03:00 committed by Vladiwostok
parent 531f75bd29
commit c0c881ed39
6 changed files with 763 additions and 1129 deletions

View 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");
}

View file

@ -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();

View file

@ -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;
}

View 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"));
}

View file

@ -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];
}
}

View file

@ -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;