620 lines
17 KiB
D
620 lines
17 KiB
D
// Copyright Brian Schott (Hackerpilot) 2012.
|
|
// 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.main;
|
|
|
|
import dparse.lexer;
|
|
import dparse.parser;
|
|
import dparse.rollback_allocator;
|
|
import std.algorithm;
|
|
import std.array;
|
|
import std.conv;
|
|
import std.experimental.lexer;
|
|
import std.file;
|
|
import std.functional : toDelegate;
|
|
import std.getopt;
|
|
import std.path;
|
|
import std.range;
|
|
import std.stdio;
|
|
import std.string : chomp, splitLines;
|
|
import std.typecons : scoped;
|
|
|
|
import dscanner.highlighter;
|
|
import dscanner.stats;
|
|
import dscanner.ctags;
|
|
import dscanner.etags;
|
|
import dscanner.astprinter;
|
|
import dscanner.imports;
|
|
import dscanner.outliner;
|
|
import dscanner.symbol_finder;
|
|
import dscanner.analysis.run;
|
|
import dscanner.analysis.config;
|
|
import dscanner.dscanner_version;
|
|
import dscanner.utils;
|
|
|
|
import inifiled;
|
|
|
|
import dsymbol.modulecache;
|
|
|
|
version (unittest)
|
|
void main()
|
|
{
|
|
}
|
|
else
|
|
int main(string[] args)
|
|
{
|
|
bool autofix;
|
|
bool sloc;
|
|
bool highlight;
|
|
bool ctags;
|
|
bool etags;
|
|
bool etagsAll;
|
|
bool help;
|
|
bool tokenCount;
|
|
bool syntaxCheck;
|
|
bool ast;
|
|
bool imports;
|
|
bool recursiveImports;
|
|
bool muffin;
|
|
bool outline;
|
|
bool tokenDump;
|
|
bool styleCheck;
|
|
bool defaultConfig;
|
|
bool report;
|
|
bool skipTests;
|
|
bool applySingleFixes;
|
|
string resolveMessage;
|
|
string reportFormat;
|
|
string reportFile;
|
|
string symbolName;
|
|
string configLocation;
|
|
string[] importPaths;
|
|
bool printVersion;
|
|
bool explore;
|
|
bool verbose;
|
|
string errorFormat;
|
|
|
|
if (args.length == 2 && args[1].startsWith("@"))
|
|
args = args[0] ~ readText(args[1][1 .. $]).chomp.splitLines;
|
|
|
|
try
|
|
{
|
|
// dfmt off
|
|
getopt(args, std.getopt.config.caseSensitive,
|
|
"sloc|l", &sloc,
|
|
"highlight", &highlight,
|
|
"ctags|c", &ctags,
|
|
"help|h", &help,
|
|
"etags|e", &etags,
|
|
"etagsAll", &etagsAll,
|
|
"tokenCount|t", &tokenCount,
|
|
"syntaxCheck|s", &syntaxCheck,
|
|
"ast|xml", &ast,
|
|
"imports|i", &imports,
|
|
"recursiveImports", &recursiveImports,
|
|
"outline|o", &outline,
|
|
"tokenDump", &tokenDump,
|
|
"styleCheck|S", &styleCheck,
|
|
"defaultConfig", &defaultConfig,
|
|
"declaration|d", &symbolName,
|
|
"config", &configLocation,
|
|
"report", &report,
|
|
"reportFormat", &reportFormat,
|
|
"reportFile", &reportFile,
|
|
"resolveMessage", &resolveMessage,
|
|
"applySingle", &applySingleFixes,
|
|
"I", &importPaths,
|
|
"version", &printVersion,
|
|
"muffinButton", &muffin,
|
|
"explore", &explore,
|
|
"skipTests", &skipTests,
|
|
"errorFormat|f", &errorFormat,
|
|
"verbose|v", &verbose
|
|
);
|
|
//dfmt on
|
|
}
|
|
catch (ConvException e)
|
|
{
|
|
stderr.writeln(e.msg);
|
|
return 1;
|
|
}
|
|
catch (GetOptException e)
|
|
{
|
|
stderr.writeln(e.msg);
|
|
return 1;
|
|
}
|
|
|
|
{
|
|
static if (__VERSION__ >= 2_101)
|
|
import std.logger : sharedLog, LogLevel;
|
|
else
|
|
import std.experimental.logger : globalLogLevel, LogLevel;
|
|
// we don't use std.logger, but dsymbol does, so we surpress all
|
|
// messages that aren't errors from it by default
|
|
// users can use verbose to enable all logs (this will log things like
|
|
// dsymbol couldn't find some modules due to wrong import paths)
|
|
static if (__VERSION__ >= 2_101)
|
|
(cast()sharedLog).logLevel = verbose ? LogLevel.all : LogLevel.error;
|
|
else
|
|
globalLogLevel = verbose ? LogLevel.all : LogLevel.error;
|
|
}
|
|
|
|
if (muffin)
|
|
{
|
|
stdout.writeln(` ___________
|
|
__(#*O 0** @%*)__
|
|
_(%*o#*O%*0 #O#%##@)_
|
|
(*#@%#o*@ #o%O*%@ #o #)
|
|
\=====================/
|
|
|I|I|I|I|I|I|I|I|I|I|
|
|
|I|I|I|I|I|I|I|I|I|I|
|
|
|I|I|I|I|I|I|I|I|I|I|
|
|
|I|I|I|I|I|I|I|I|I|I|`);
|
|
return 0;
|
|
}
|
|
|
|
if (explore)
|
|
{
|
|
stdout.writeln("D-Scanner: Scanning...");
|
|
stderr.writeln("D-Scanner: No new astronomical objects discovered.");
|
|
return 1;
|
|
}
|
|
|
|
if (help)
|
|
{
|
|
printHelp(args[0]);
|
|
return 0;
|
|
}
|
|
|
|
if (printVersion)
|
|
{
|
|
writeln(DSCANNER_VERSION);
|
|
return 0;
|
|
}
|
|
|
|
if (args.length > 1)
|
|
{
|
|
switch (args[1])
|
|
{
|
|
case "lint":
|
|
args = args[0] ~ args[2 .. $];
|
|
styleCheck = true;
|
|
if (!errorFormat.length)
|
|
errorFormat = "pretty";
|
|
break;
|
|
case "fix":
|
|
args = args[0] ~ args[2 .. $];
|
|
autofix = true;
|
|
if (!errorFormat.length)
|
|
errorFormat = "pretty";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!errorFormat.length)
|
|
errorFormat = defaultErrorFormat;
|
|
else if (auto errorFormatSuppl = errorFormat in errorFormatMap)
|
|
errorFormat = (*errorFormatSuppl)
|
|
// support some basic formatting things so it's easier for the user to type these
|
|
.replace("\\x1B", "\x1B")
|
|
.replace("\\033", "\x1B")
|
|
.replace("\\r", "\r")
|
|
.replace("\\n", "\n")
|
|
.replace("\\t", "\t");
|
|
|
|
const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath()
|
|
.buildNormalizedPath()).array();
|
|
|
|
ModuleCache moduleCache;
|
|
|
|
if (absImportPaths.length)
|
|
moduleCache.addImportPaths(absImportPaths);
|
|
|
|
if (reportFormat.length || reportFile.length)
|
|
report = true;
|
|
|
|
immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount,
|
|
syntaxCheck, ast, imports, outline, tokenDump, styleCheck,
|
|
defaultConfig, report, autofix, resolveMessage.length,
|
|
symbolName !is null, etags, etagsAll, recursiveImports,
|
|
]);
|
|
if (optionCount > 1)
|
|
{
|
|
stderr.writeln("Too many options specified");
|
|
return 1;
|
|
}
|
|
else if (optionCount < 1)
|
|
{
|
|
printHelp(args[0]);
|
|
return 1;
|
|
}
|
|
|
|
// --report implies --styleCheck
|
|
if (report)
|
|
styleCheck = true;
|
|
|
|
immutable usingStdin = args.length == 1;
|
|
|
|
StringCache cache = StringCache(StringCache.defaultBucketCount);
|
|
if (defaultConfig)
|
|
{
|
|
string s = getConfigurationLocation();
|
|
mkdirRecurse(findSplitBefore(s, "dscanner.ini")[0]);
|
|
StaticAnalysisConfig saConfig = defaultStaticAnalysisConfig();
|
|
writeln("Writing default config file to ", s);
|
|
writeINIFile(saConfig, s);
|
|
}
|
|
else if (tokenDump || highlight)
|
|
{
|
|
ubyte[] bytes = usingStdin ? readStdin() : readFile(args[1]);
|
|
LexerConfig config;
|
|
config.stringBehavior = StringBehavior.source;
|
|
|
|
if (highlight)
|
|
{
|
|
auto tokens = byToken(bytes, config, &cache);
|
|
dscanner.highlighter.highlight(tokens, args.length == 1 ? "stdin" : args[1]);
|
|
return 0;
|
|
}
|
|
else if (tokenDump)
|
|
{
|
|
auto tokens = getTokensForParser(bytes, config, &cache);
|
|
writeln(
|
|
"text \tblank\tindex\tline\tcolumn\ttype\tcomment\ttrailingComment");
|
|
foreach (token; tokens)
|
|
{
|
|
writefln("<<%20s>>\t%b\t%d\t%d\t%d\t%d\t%s\t%s",
|
|
token.text is null ? str(token.type) : token.text,
|
|
token.text is null,
|
|
token.index,
|
|
token.line,
|
|
token.column,
|
|
token.type,
|
|
token.comment,
|
|
token.trailingComment);
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
else if (symbolName !is null)
|
|
{
|
|
stdout.findDeclarationOf(symbolName, expandArgs(args));
|
|
}
|
|
else if (ctags)
|
|
{
|
|
stdout.printCtags(expandArgs(args));
|
|
}
|
|
else if (etags || etagsAll)
|
|
{
|
|
stdout.printEtags(etagsAll, expandArgs(args));
|
|
}
|
|
else if (styleCheck || autofix || resolveMessage.length)
|
|
{
|
|
StaticAnalysisConfig config = defaultStaticAnalysisConfig();
|
|
string s = configLocation is null ? getConfigurationLocation() : configLocation;
|
|
if (s.exists())
|
|
readINIFile(config, s);
|
|
if (skipTests)
|
|
config.enabled2SkipTests;
|
|
|
|
if (autofix)
|
|
{
|
|
return .autofix(expandArgs(args), config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0;
|
|
}
|
|
else if (resolveMessage.length)
|
|
{
|
|
listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1], &cache, moduleCache);
|
|
return 0;
|
|
}
|
|
else if (report)
|
|
{
|
|
switch (reportFormat)
|
|
{
|
|
default:
|
|
stderr.writeln("Unknown report format specified, using dscanner format");
|
|
goto case;
|
|
case "":
|
|
case "dscanner":
|
|
generateReport(expandArgs(args), config, cache, moduleCache, reportFile);
|
|
break;
|
|
case "sonarQubeGenericIssueData":
|
|
generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache, reportFile);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
return analyze(expandArgs(args), config, errorFormat, cache, moduleCache, true) ? 1 : 0;
|
|
}
|
|
else if (syntaxCheck)
|
|
{
|
|
return .syntaxCheck(usingStdin ? ["stdin"] : expandArgs(args), errorFormat, cache, moduleCache) ? 1 : 0;
|
|
}
|
|
else
|
|
{
|
|
if (sloc || tokenCount)
|
|
{
|
|
if (usingStdin)
|
|
{
|
|
LexerConfig config;
|
|
config.stringBehavior = StringBehavior.source;
|
|
auto tokens = byToken(readStdin(), config, &cache);
|
|
if (tokenCount)
|
|
printTokenCount(stdout, "stdin", tokens);
|
|
else
|
|
printLineCount(stdout, "stdin", tokens);
|
|
}
|
|
else
|
|
{
|
|
ulong count;
|
|
foreach (f; expandArgs(args))
|
|
{
|
|
|
|
LexerConfig config;
|
|
config.stringBehavior = StringBehavior.source;
|
|
auto tokens = byToken(readFile(f), config, &cache);
|
|
if (tokenCount)
|
|
count += printTokenCount(stdout, f, tokens);
|
|
else
|
|
count += printLineCount(stdout, f, tokens);
|
|
}
|
|
writefln("total:\t%d", count);
|
|
}
|
|
}
|
|
else if (imports || recursiveImports)
|
|
{
|
|
printImports(usingStdin, args, importPaths, &cache, recursiveImports);
|
|
}
|
|
else if (ast || outline)
|
|
{
|
|
string fileName = usingStdin ? "stdin" : args[1];
|
|
RollbackAllocator rba;
|
|
LexerConfig config;
|
|
config.fileName = fileName;
|
|
config.stringBehavior = StringBehavior.source;
|
|
auto tokens = getTokensForParser(usingStdin ? readStdin()
|
|
: readFile(args[1]), config, &cache);
|
|
auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing));
|
|
|
|
if (ast)
|
|
{
|
|
auto printer = new XMLPrinter;
|
|
printer.output = stdout;
|
|
printer.visit(mod);
|
|
}
|
|
else if (outline)
|
|
{
|
|
auto outliner = new Outliner(stdout);
|
|
outliner.visit(mod);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void printHelp(string programName)
|
|
{
|
|
stderr.writefln(`
|
|
Usage: %1$s <options>
|
|
|
|
Human-readable output:
|
|
%1$s lint <options> <files...>
|
|
|
|
Interactively fixing issues
|
|
%1$s fix [--applySingle] <files...>
|
|
|
|
Parsable outputs:
|
|
%1$s -S <options> <files...>
|
|
%1$s --report <options> <files...>
|
|
|
|
Options:
|
|
--help, -h
|
|
Prints this help message
|
|
|
|
--version
|
|
Prints the program version
|
|
|
|
--sloc <file | directory>..., -l <file | directory>...
|
|
Prints the number of logical lines of code in the given
|
|
source files. If no files are specified, input is read from stdin.
|
|
|
|
--tokenCount <file | directory>..., -t <file | directory>...
|
|
Prints the number of tokens in the given source files. If no files are
|
|
specified, input is read from stdin.
|
|
|
|
--tokenDump <file>
|
|
Dump token information from the lexer. This option is mostly useful for
|
|
developing D-Scanner or its supporting libraries itself. You probably
|
|
dont't want to use this, and this feature may be removed in the future.
|
|
|
|
--highlight <file>
|
|
Syntax-highlight the given source file. The resulting HTML will be
|
|
written to standard output. If no file is specified, input is read
|
|
from stdin.
|
|
|
|
--imports <file>, -i <file>
|
|
Prints modules imported by the given source file. If no files are
|
|
specified, input is read from stdin. Combine with "-I" arguments to
|
|
resolve import locations.
|
|
|
|
--recursiveImports <file>
|
|
Similar to "--imports", but lists imports of imports recursively.
|
|
|
|
-I <directory>
|
|
Specify that the given directory should be searched for imported
|
|
modules. This option can be passed multiple times to specify multiple
|
|
directories.
|
|
|
|
--syntaxCheck <file>, -s <file>
|
|
Lexes and parses sourceFile, printing the line and column number of
|
|
any syntax errors to stdout. One error or warning is printed per line,
|
|
and formatted according to the pattern passed to "--errorFormat".
|
|
If no files are specified, input is read from stdin. %1$s will exit
|
|
with a status code of zero if no errors are found, 1 otherwise.
|
|
|
|
--styleCheck|S <file | directory>..., <file | directory>...
|
|
Lexes and parses sourceFiles, printing the line and column number of
|
|
any static analysis check failures stdout. One error or warning is
|
|
printed per line, and formatted according to the pattern passed to
|
|
"--errorFormat". %1$s will exit with a status code of zero if no
|
|
warnings or errors are found, 1 otherwise.
|
|
|
|
--errorFormat|f <pattern>
|
|
Format errors produced by the style/syntax checkers. The default
|
|
value for the pattern is: "%2$s".
|
|
|
|
Supported placeholders are:
|
|
- {filepath}: file path, usually relative to CWD
|
|
- {line}: start line number, 1-based
|
|
- {endLine}: end line number, 1-based, inclusive
|
|
- {column}: start column on start line, 1-based, in bytes
|
|
- {endColumn}: end column on end line, 1-based, in bytes, exclusive
|
|
- {startIndex}: start file byte offset, 0-based
|
|
- {endIndex}: end file byte offset, 0-based
|
|
- {type}: "error" or "warn", uppercase variants: {Type}, {TYPE},
|
|
- {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2}
|
|
- {message}: human readable message such as "Variable c is never used."
|
|
- {name}: D-Scanner check name such as "unused_variable_check"
|
|
- {context}: "\n<source code>\n ^^^^^ here"
|
|
- {supplemental}: for supplemental messages, each one formatted using
|
|
this same format string, tab indented, type = "hint".
|
|
|
|
For compatibility with other tools, the following strings may be
|
|
specified as shorthand aliases:
|
|
|
|
%3$(-f %1$s -> %2$s` ~ '\n' ~ ` %)
|
|
|
|
When calling "%1$s lint" for human readable output, "pretty"
|
|
is used by default.
|
|
|
|
--ctags <file | directory>..., -c <file | directory>...
|
|
Generates ctags information from the given source code file. Note that
|
|
ctags information requires a filename, so stdin cannot be used in place
|
|
of a filename.
|
|
|
|
--etags <file | directory>..., -e <file | directory>...
|
|
Generates etags information from the given source code file. Note that
|
|
etags information requires a filename, so stdin cannot be used in place
|
|
of a filename.
|
|
|
|
--etagsAll <file | directory>...
|
|
Same as --etags except private and package declarations are tagged too.
|
|
|
|
--ast <file> | --xml <file>
|
|
Generates an XML representation of the source files abstract syntax
|
|
tree. If no files are specified, input is read from stdin.
|
|
|
|
--declaration <symbolName> <file | directory>...,
|
|
-d <symbolName> <file | directory>...
|
|
Find the location where symbolName is declared. This should be more
|
|
accurate than "grep". Searches the given files and directories, or the
|
|
current working directory if none are specified.
|
|
|
|
--report <file | directory>...
|
|
Generate a static analysis report in JSON format. Implies --styleCheck,
|
|
however the exit code will still be zero if errors or warnings are
|
|
found.
|
|
|
|
--reportFile <file>
|
|
Write report into file instead of STDOUT.
|
|
|
|
--reportFormat <dscanner | sonarQubeGenericIssueData>...
|
|
Specifies the format of the generated report.
|
|
|
|
--config <file>
|
|
Use the given configuration file instead of the default located in
|
|
$HOME/.config/dscanner/dscanner.ini
|
|
|
|
--defaultConfig
|
|
Generates a default configuration file for the static analysis checks,
|
|
|
|
--skipTests
|
|
Does not analyze code in unittests. Only works if --styleCheck
|
|
is specified.
|
|
|
|
--applySingle
|
|
when running "dscanner fix", automatically apply all fixes that have
|
|
only one auto-fix.`,
|
|
|
|
programName, defaultErrorFormat, errorFormatMap);
|
|
}
|
|
|
|
private void doNothing(string, size_t, size_t, string, bool)
|
|
{
|
|
}
|
|
|
|
private enum CONFIG_FILE_NAME = "dscanner.ini";
|
|
version (linux) version = useXDG;
|
|
version (BSD) version = useXDG;
|
|
version (FreeBSD) version = useXDG;
|
|
version (OSX) version = useXDG;
|
|
|
|
/**
|
|
* Locates the default configuration file
|
|
*/
|
|
string getDefaultConfigurationLocation()
|
|
{
|
|
import std.process : environment;
|
|
import std.exception : enforce;
|
|
version (useXDG)
|
|
{
|
|
string configDir = environment.get("XDG_CONFIG_HOME", null);
|
|
if (configDir is null)
|
|
{
|
|
configDir = environment.get("HOME", null);
|
|
enforce(configDir !is null, "Both $XDG_CONFIG_HOME and $HOME are unset");
|
|
configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME);
|
|
}
|
|
else
|
|
configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
|
|
return configDir;
|
|
}
|
|
else version(Windows)
|
|
{
|
|
string configDir = environment.get("APPDATA", null);
|
|
enforce(configDir !is null, "%APPDATA% is unset");
|
|
configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
|
|
return configDir;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Searches upwards from the CWD through the directory hierarchy
|
|
*/
|
|
string tryFindConfigurationLocation()
|
|
{
|
|
auto path = pathSplitter(getcwd());
|
|
string result;
|
|
|
|
while (!path.empty)
|
|
{
|
|
result = buildPath(buildPath(path), CONFIG_FILE_NAME);
|
|
|
|
if (exists(result))
|
|
break;
|
|
|
|
path.popBack();
|
|
}
|
|
|
|
if (path.empty)
|
|
return null;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Tries to find a config file and returns the default one on failure
|
|
*/
|
|
string getConfigurationLocation()
|
|
{
|
|
immutable config = tryFindConfigurationLocation();
|
|
|
|
if (config !is null)
|
|
return config;
|
|
|
|
return getDefaultConfigurationLocation();
|
|
}
|