├── tests ├── it │ ├── autofix_cli │ │ ├── .gitignore │ │ ├── fixed.d │ │ └── source.d │ ├── singleissue.d │ ├── autofix_ide │ │ ├── source_autofix.d │ │ ├── source_autofix.autofix.json │ │ └── source_autofix.report.json │ └── singleissue_github.txt ├── it.sh └── dscanner.ini ├── .gitattributes ├── dsc.sublime-project ├── sonar-project.properties ├── scripts ├── uml.sh ├── importgraph.sh ├── README.md └── uml.xsl ├── Dockerfile ├── src └── dscanner │ ├── analysis │ ├── package.d │ ├── comma_expression.d │ ├── stats_collector.d │ ├── alias_syntax_check.d │ ├── asm_style.d │ ├── redundant_parens.d │ ├── del.d │ ├── logic_precedence.d │ ├── numbers.d │ ├── fish.d │ ├── length_subtraction.d │ ├── enumarrayliteral.d │ ├── local_imports.d │ ├── unused_variable.d │ ├── pokemon.d │ ├── unused_parameter.d │ ├── builtin_property_names.d │ ├── auto_ref_assignment.d │ ├── explicitly_annotated_unittests.d │ ├── useless_assert.d │ ├── redundant_storage_class.d │ ├── allman.d │ ├── constructors.d │ ├── if_statements.d │ ├── lambda_return_check.d │ ├── ifelsesame.d │ ├── trust_too_much.d │ ├── static_if_else.d │ ├── assert_without_msg.d │ ├── incorrect_infinite_range.d │ ├── range.d │ ├── objectconst.d │ ├── opequals_without_tohash.d │ ├── unused_result.d │ ├── always_curly.d │ ├── unused_label.d │ ├── label_var_same_name_check.d │ ├── body_on_disabled_funcs.d │ ├── style.d │ └── nolint.d │ ├── dscanner_version.d │ ├── stats.d │ ├── highlighter.d │ ├── imports.d │ ├── symbol_finder.d │ └── outliner.d ├── dub.selections.json ├── .gitignore ├── release-windows.sh ├── .gitmodules ├── .editorconfig ├── dub.json ├── release.sh ├── dubhash.d ├── LICENSE_1_0.txt ├── setup-ldc-windows.sh ├── dscanner.ini ├── makefile ├── appveyor.yml └── .github └── workflows └── default.yml /tests/it/autofix_cli/.gitignore: -------------------------------------------------------------------------------- 1 | test.d 2 | -------------------------------------------------------------------------------- /tests/it/singleissue.d: -------------------------------------------------------------------------------- 1 | int NonMatchingName; 2 | -------------------------------------------------------------------------------- /tests/it/autofix_cli/fixed.d: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | } 4 | -------------------------------------------------------------------------------- /tests/it/autofix_cli/source.d: -------------------------------------------------------------------------------- 1 | auto main() 2 | { 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/it/autofix_ide/source_autofix.d text eol=lf -------------------------------------------------------------------------------- /dsc.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "build_system": "Packages/Makefile/Make.sublime-build", 3 | "folders": 4 | [ 5 | { 6 | "path": "." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=dscanner 2 | sonar.projectName=D Scanner 3 | sonar.projectVersion=1.0 4 | sonar.sourceEncoding=UTF-8 5 | sonar.sources=src 6 | -------------------------------------------------------------------------------- /scripts/uml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | dscanner $1 --ast | xsltproc uml.xsl - | dot -Tpng > $(basename $1 .d).png 3 | #dscanner $1 --ast | xsltproc uml.xsl - | dot -Tpng | display - -------------------------------------------------------------------------------- /tests/it/autofix_ide/source_autofix.d: -------------------------------------------------------------------------------- 1 | struct S 2 | { 3 | int myProp() @property 4 | { 5 | static if (a) 6 | { 7 | } 8 | else if (b) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/it/singleissue_github.txt: -------------------------------------------------------------------------------- 1 | ::warning file=it/singleissue.d,line=1,endLine=1,col=5,endColumn=20,title=Warning (style_check)::Variable name 'NonMatchingName' does not match style guidelines. 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | 3 | MAINTAINER "DLang Community " 4 | 5 | COPY bin/dscanner /dscanner 6 | RUN chmod +x /dscanner 7 | 8 | WORKDIR /src 9 | 10 | ENTRYPOINT [ "/dscanner" ] 11 | -------------------------------------------------------------------------------- /src/dscanner/analysis/package.d: -------------------------------------------------------------------------------- 1 | module dscanner.analysis; 2 | 3 | public import dscanner.analysis.style; 4 | public import dscanner.analysis.enumarrayliteral; 5 | public import dscanner.analysis.pokemon; 6 | public import dscanner.analysis.base; 7 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dcd": "0.16.0-beta.2", 5 | "dsymbol": "0.13.0", 6 | "emsi_containers": "0.9.0", 7 | "inifiled": "1.3.3", 8 | "libddoc": "0.8.0", 9 | "libdparse": "0.25.0", 10 | "stdx-allocator": "2.77.5" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/importgraph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | output=$(echo "digraph {") 3 | for i in "$@"; do 4 | m=$(echo $i | sed -e "s/^\.\///" -e "s/\//\./g" -e "s/\.d$//") 5 | output=$output$(dscanner --imports $i 2>/dev/null | sort | uniq | xargs -I{} echo "\"" $m "\"->\"" {} "\";") 6 | done 7 | output=$output$(echo "}") 8 | echo $output | unflatten -l 3 -f | dot -Tpng > out.png 9 | display out.png 10 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Extra Scripts 2 | Extra scripts that use DScanner to produce their output. These scripts assume a UNIX environment. 3 | 4 | ### importgraph.sh 5 | Displays a graph of the imports of the given D files. 6 | Requires Imagemagick and Graphviz 7 | 8 | ### uml.sh 9 | Displays a basic class diagram of classes, structs, and interfaces in the given D file. 10 | Requires Imagemagic, XSLTProc, and Graphviz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Vim swap file 2 | *.swp 3 | 4 | # Backup files 5 | *~ 6 | 7 | # Mono-D files 8 | *.pidb 9 | *.userprefs 10 | 11 | # Sublime Text 2 12 | *.sublime-workspace 13 | 14 | # Subversion 15 | .svn/ 16 | 17 | # D Scanner binaries 18 | /bin 19 | 20 | # Static analysis reports 21 | dscanner-report.json 22 | 23 | # Object files 24 | *.o 25 | *.obj 26 | *.exe 27 | obj 28 | 29 | # debug build 30 | dsc 31 | 32 | # GDB history 33 | .gdb_history 34 | 35 | # Dub stuff 36 | .dub 37 | -------------------------------------------------------------------------------- /release-windows.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Build the Windows binaries under Linux 3 | set -eux -o pipefail 4 | 5 | BIN_NAME=dscanner 6 | 7 | # Allow the script to be run from anywhere 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | cd $DIR 10 | 11 | source setup-ldc-windows.sh 12 | 13 | # Run LDC with cross-compilation 14 | archiveName="$BIN_NAME-$VERSION-$OS-$ARCH_SUFFIX.zip" 15 | echo "Building $archiveName" 16 | mkdir -p bin 17 | DC=ldmd2 make ldcbuild 18 | 19 | cd bin 20 | mv "${BIN_NAME}" "${BIN_NAME}.exe" 21 | zip "$archiveName" "${BIN_NAME}.exe" 22 | -------------------------------------------------------------------------------- /tests/it/autofix_ide/source_autofix.autofix.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Mark function `const`", 4 | "replacements": [ 5 | { 6 | "newText": " const", 7 | "range": [ 8 | 24, 9 | 24 10 | ] 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "Mark function `inout`", 16 | "replacements": [ 17 | { 18 | "newText": " inout", 19 | "range": [ 20 | 24, 21 | 24 22 | ] 23 | } 24 | ] 25 | }, 26 | { 27 | "name": "Mark function `immutable`", 28 | "replacements": [ 29 | { 30 | "newText": " immutable", 31 | "range": [ 32 | 24, 33 | 24 34 | ] 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /src/dscanner/dscanner_version.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.dscanner_version; 7 | 8 | import std.string : strip; 9 | 10 | /** 11 | * Human-readable version number 12 | */ 13 | 14 | version (built_with_dub) 15 | { 16 | enum DSCANNER_VERSION = import("dubhash.txt").strip; 17 | } 18 | else 19 | { 20 | /** 21 | * Current build's Git commit hash 22 | */ 23 | enum DSCANNER_VERSION = import("githash.txt").strip; 24 | } 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libdparse"] 2 | path = libdparse 3 | url = https://github.com/dlang-community/libdparse.git 4 | branch = master 5 | [submodule "inifiled"] 6 | path = inifiled 7 | url = https://github.com/burner/inifiled.git 8 | [submodule "containers"] 9 | path = containers 10 | url = https://github.com/dlang-community/containers.git 11 | [submodule "libddoc"] 12 | path = libddoc 13 | url = https://github.com/dlang-community/libddoc.git 14 | [submodule "d-test-utils"] 15 | path = d-test-utils 16 | url = https://github.com/dlang-community/d-test-utils.git 17 | [submodule "DCD"] 18 | path = DCD 19 | url = https://github.com/dlang-community/DCD.git 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_size = 4 5 | tab_width = 4 6 | trim_trailing_whitespace = true 7 | indent_style = tab 8 | charset = UTF-8 9 | max_line_length = 120 10 | 11 | [*.d] 12 | dfmt_brace_style = allman 13 | dfmt_soft_max_line_length = 80 14 | dfmt_align_switch_statements = true 15 | dfmt_outdent_attributes = true 16 | dfmt_split_operator_at_end_of_line = true 17 | dfmt_space_after_cast = true 18 | dfmt_space_after_keywords = true 19 | dfmt_selective_import_space = true 20 | dfmt_compact_labeled_statements = true 21 | dfmt_template_constraint_style = conditional_newline_indent 22 | 23 | [*.yml] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "dscanner", 3 | "description" : "Swiss-army knife for D source code", 4 | "copyright" : "© Brian Schott", 5 | "authors" : [ 6 | "Brian Schott" 7 | ], 8 | "license" : "BSL-1.0", 9 | "targetType" : "autodetect", 10 | "versions" : [ 11 | "built_with_dub" 12 | ], 13 | "dependencies": { 14 | "libdparse": ">=0.23.1 <0.26.0", 15 | "dcd:dsymbol": ">=0.16.0-beta.2 <0.17.0", 16 | "inifiled": "~>1.3.1", 17 | "emsi_containers": "~>0.9.0", 18 | "libddoc": "~>0.8.0" 19 | }, 20 | "targetPath" : "bin", 21 | "stringImportPaths" : [ 22 | "bin" 23 | ], 24 | "preBuildCommands" : [ 25 | "\"$DC\" -run \"$PACKAGE_DIR/dubhash.d\"" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux -o pipefail 3 | VERSION=$(git describe --abbrev=0 --tags) 4 | ARCH="${ARCH:-64}" 5 | LDC_FLAGS=() 6 | unameOut="$(uname -s)" 7 | case "$unameOut" in 8 | Linux*) OS=linux; LDC_FLAGS=("-flto=full" "-linker=gold" "-static") ;; 9 | Darwin*) OS=osx; LDC_FLAGS+=("-L-macosx_version_min" "-L10.7" "-L-lcrt1.o"); ;; 10 | *) echo "Unknown OS: $unameOut"; exit 1 11 | esac 12 | 13 | case "$ARCH" in 14 | 64) ARCH_SUFFIX="x86_64";; 15 | 32) ARCH_SUFFIX="x86";; 16 | *) echo "Unknown ARCH: $ARCH"; exit 1 17 | esac 18 | 19 | archiveName="dscanner-$VERSION-$OS-$ARCH_SUFFIX.tar.gz" 20 | 21 | echo "Building $archiveName" 22 | ${MAKE:-make} ldcbuild LDC_FLAGS="${LDC_FLAGS[*]}" 23 | tar cvfz "bin/$archiveName" -C bin dscanner 24 | -------------------------------------------------------------------------------- /dubhash.d: -------------------------------------------------------------------------------- 1 | import std.algorithm; 2 | import std.ascii; 3 | import std.conv; 4 | import std.exception; 5 | import std.file; 6 | import std.path; 7 | import std.process; 8 | import std.range; 9 | import std.string; 10 | 11 | void main() 12 | { 13 | auto dir = environment.get("DUB_PACKAGE_DIR"); 14 | auto hashFile = dir.buildPath("bin", "dubhash.txt"); 15 | auto gitVer = executeShell("git -C " ~ dir ~ " describe --tags"); 16 | auto ver = (gitVer.status == 0 ? gitVer.output.strip 17 | : "v" ~ dir.dirName.baseName.findSplitAfter( 18 | environment.get("DUB_ROOT_PACKAGE") ~ "-")[1]).ifThrown("0.0.0") 19 | .chain(newline).to!string.strip; 20 | dir.buildPath("bin").mkdirRecurse; 21 | if (!hashFile.exists || ver != hashFile.readText.strip) 22 | hashFile.write(ver); 23 | } 24 | -------------------------------------------------------------------------------- /src/dscanner/stats.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2012. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.stats; 7 | 8 | import std.stdio; 9 | import std.algorithm; 10 | import dparse.lexer; 11 | 12 | pure nothrow bool isLineOfCode(IdType t) 13 | { 14 | switch (t) 15 | { 16 | case tok!";": 17 | case tok!"while": 18 | case tok!"if": 19 | case tok!"do": 20 | case tok!"else": 21 | case tok!"switch": 22 | case tok!"for": 23 | case tok!"foreach": 24 | case tok!"foreach_reverse": 25 | case tok!"default": 26 | case tok!"case": 27 | return true; 28 | default: 29 | return false; 30 | } 31 | } 32 | 33 | ulong printTokenCount(Tokens)(File output, string fileName, ref Tokens tokens) 34 | { 35 | ulong c; 36 | foreach (ref t; tokens) 37 | { 38 | c++; 39 | } 40 | output.writefln("%s:\t%d", fileName, c); 41 | return c; 42 | } 43 | 44 | ulong printLineCount(Tokens)(File output, string fileName, ref Tokens tokens) 45 | { 46 | ulong count; 47 | foreach (ref t; tokens) 48 | { 49 | if (isLineOfCode(t.type)) 50 | ++count; 51 | } 52 | output.writefln("%s:\t%d", fileName, count); 53 | return count; 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/dscanner/analysis/comma_expression.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.comma_expression; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import dsymbol.scope_; 12 | 13 | /** 14 | * Check for uses of the comma expression. 15 | */ 16 | final class CommaExpressionCheck : BaseAnalyzer 17 | { 18 | alias visit = BaseAnalyzer.visit; 19 | 20 | mixin AnalyzerInfo!"comma_expression_check"; 21 | 22 | this(BaseAnalyzerArguments args) 23 | { 24 | super(args); 25 | } 26 | 27 | override void visit(const Expression ex) 28 | { 29 | if (ex.items.length > 1 && interest > 0) 30 | { 31 | addErrorMessage(ex, KEY, "Avoid using the comma expression."); 32 | } 33 | ex.accept(this); 34 | } 35 | 36 | override void visit(const AssignExpression ex) 37 | { 38 | ++interest; 39 | ex.accept(this); 40 | --interest; 41 | } 42 | 43 | // Dconf 2016 44 | override void visit(const SynchronizedStatement ss) 45 | { 46 | if (ss.expression !is null) 47 | { 48 | ++interest; 49 | visit(ss.expression); 50 | --interest; 51 | } 52 | visit(ss.statementNoCaseNoDefault); 53 | } 54 | 55 | invariant 56 | { 57 | assert(interest >= 0); 58 | } 59 | 60 | int interest; 61 | 62 | private enum string KEY = "dscanner.suspicious.comma_expression"; 63 | } 64 | -------------------------------------------------------------------------------- /src/dscanner/analysis/stats_collector.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.stats_collector; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | 12 | final class StatsCollector : BaseAnalyzer 13 | { 14 | alias visit = ASTVisitor.visit; 15 | 16 | this(BaseAnalyzerArguments args) 17 | { 18 | args.skipTests = false; // old behavior compatibility 19 | super(args); 20 | } 21 | 22 | override void visit(const Statement statement) 23 | { 24 | statementCount++; 25 | statement.accept(this); 26 | } 27 | 28 | override void visit(const ClassDeclaration classDeclaration) 29 | { 30 | classCount++; 31 | classDeclaration.accept(this); 32 | } 33 | 34 | override void visit(const InterfaceDeclaration interfaceDeclaration) 35 | { 36 | interfaceCount++; 37 | interfaceDeclaration.accept(this); 38 | } 39 | 40 | override void visit(const FunctionDeclaration functionDeclaration) 41 | { 42 | functionCount++; 43 | functionDeclaration.accept(this); 44 | } 45 | 46 | override void visit(const StructDeclaration structDeclaration) 47 | { 48 | structCount++; 49 | structDeclaration.accept(this); 50 | } 51 | 52 | override void visit(const TemplateDeclaration templateDeclaration) 53 | { 54 | templateCount++; 55 | templateDeclaration.accept(this); 56 | } 57 | 58 | uint interfaceCount; 59 | uint classCount; 60 | uint functionCount; 61 | uint templateCount; 62 | uint structCount; 63 | uint statementCount; 64 | uint lineOfCodeCount; 65 | uint undocumentedPublicSymbols; 66 | } 67 | -------------------------------------------------------------------------------- /src/dscanner/analysis/alias_syntax_check.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2016. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.alias_syntax_check; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | 12 | /** 13 | * Checks for uses of the old alias syntax. 14 | */ 15 | final class AliasSyntaxCheck : BaseAnalyzer 16 | { 17 | alias visit = BaseAnalyzer.visit; 18 | 19 | mixin AnalyzerInfo!"alias_syntax_check"; 20 | 21 | this(BaseAnalyzerArguments args) 22 | { 23 | super(args); 24 | } 25 | 26 | override void visit(const AliasDeclaration ad) 27 | { 28 | if (ad.declaratorIdentifierList is null) 29 | return; 30 | assert(ad.declaratorIdentifierList.identifiers.length > 0, 31 | "Identifier list length is zero, libdparse has a bug"); 32 | addErrorMessage(ad, KEY, 33 | "Prefer the new \"'alias' identifier '=' type ';'\" syntax" 34 | ~ " to the old \"'alias' type identifier ';'\" syntax."); 35 | } 36 | 37 | private: 38 | enum KEY = "dscanner.style.alias_syntax"; 39 | } 40 | 41 | unittest 42 | { 43 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 44 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 45 | import std.stdio : stderr; 46 | 47 | StaticAnalysisConfig sac = disabledConfig(); 48 | sac.alias_syntax_check = Check.enabled; 49 | assertAnalyzerWarnings(q{ 50 | alias int abcde; /+ 51 | ^^^^^^^^^^^^^^^^ [warn]: Prefer the new "'alias' identifier '=' type ';'" syntax to the old "'alias' type identifier ';'" syntax.+/ 52 | alias abcde = int; 53 | }c, sac); 54 | 55 | stderr.writeln("Unittest for AliasSyntaxCheck passed."); 56 | } 57 | -------------------------------------------------------------------------------- /src/dscanner/analysis/asm_style.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.asm_style; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_ : Scope; 14 | 15 | /** 16 | * Checks for confusing asm expressions. 17 | * See_also: $(LINK https://issues.dlang.org/show_bug.cgi?id=9738) 18 | */ 19 | final class AsmStyleCheck : BaseAnalyzer 20 | { 21 | alias visit = BaseAnalyzer.visit; 22 | 23 | mixin AnalyzerInfo!"asm_style_check"; 24 | 25 | this(BaseAnalyzerArguments args) 26 | { 27 | super(args); 28 | } 29 | 30 | override void visit(const AsmBrExp brExp) 31 | { 32 | if (brExp.asmBrExp !is null && brExp.asmBrExp.asmUnaExp !is null 33 | && brExp.asmBrExp.asmUnaExp.asmPrimaryExp !is null) 34 | { 35 | addErrorMessage(brExp, KEY, 36 | "This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify."); 37 | } 38 | brExp.accept(this); 39 | } 40 | 41 | private enum string KEY = "dscanner.confusing.brexp"; 42 | } 43 | 44 | unittest 45 | { 46 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 47 | 48 | StaticAnalysisConfig sac = disabledConfig(); 49 | sac.asm_style_check = Check.enabled; 50 | assertAnalyzerWarnings(q{ 51 | void testAsm() 52 | { 53 | asm 54 | { 55 | mov a, someArray[1]; /+ 56 | ^^^^^^^^^^^^ [warn]: This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify. +/ 57 | add near ptr [EAX], 3; 58 | } 59 | } 60 | }c, sac); 61 | 62 | stderr.writeln("Unittest for AsmStyleCheck passed."); 63 | } 64 | -------------------------------------------------------------------------------- /src/dscanner/analysis/redundant_parens.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.redundant_parens; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import dsymbol.scope_ : Scope; 12 | 13 | /** 14 | * Checks for redundant parenthesis 15 | */ 16 | final class RedundantParenCheck : BaseAnalyzer 17 | { 18 | alias visit = BaseAnalyzer.visit; 19 | 20 | mixin AnalyzerInfo!"redundant_parens_check"; 21 | 22 | /// 23 | this(BaseAnalyzerArguments args) 24 | { 25 | super(args); 26 | } 27 | 28 | override void visit(const IfStatement statement) 29 | { 30 | UnaryExpression unary; 31 | if (statement.condition.expression is null || statement.condition.expression.items.length != 1) 32 | goto end; 33 | unary = cast(UnaryExpression) statement.condition.expression.items[0]; 34 | if (unary is null) 35 | goto end; 36 | if (unary.primaryExpression is null) 37 | goto end; 38 | if (unary.primaryExpression.expression is null) 39 | goto end; 40 | addErrorMessage(unary.primaryExpression, KEY, "Redundant parenthesis."); 41 | end: 42 | statement.accept(this); 43 | } 44 | 45 | override void visit(const PrimaryExpression primaryExpression) 46 | { 47 | UnaryExpression unary; 48 | if (primaryExpression.expression is null) 49 | goto end; 50 | unary = cast(UnaryExpression) primaryExpression.expression.items[0]; 51 | if (unary is null) 52 | goto end; 53 | if (unary.primaryExpression is null) 54 | goto end; 55 | if (unary.primaryExpression.expression is null) 56 | goto end; 57 | addErrorMessage(primaryExpression, KEY, "Redundant parenthesis."); 58 | end: 59 | primaryExpression.accept(this); 60 | } 61 | 62 | private: 63 | enum string KEY = "dscanner.suspicious.redundant_parens"; 64 | } 65 | -------------------------------------------------------------------------------- /src/dscanner/analysis/del.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.del; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dsymbol.scope_; 13 | 14 | /** 15 | * Checks for use of the deprecated 'delete' keyword 16 | */ 17 | final class DeleteCheck : BaseAnalyzer 18 | { 19 | alias visit = BaseAnalyzer.visit; 20 | 21 | mixin AnalyzerInfo!"delete_check"; 22 | 23 | this(BaseAnalyzerArguments args) 24 | { 25 | super(args); 26 | } 27 | 28 | override void visit(const DeleteExpression d) 29 | { 30 | addErrorMessage(d.tokens[0], KEY, 31 | "Avoid using the 'delete' keyword.", 32 | [AutoFix.replacement(d.tokens[0], `destroy(`, "Replace delete with destroy()") 33 | .concat(AutoFix.insertionAfter(d.tokens[$ - 1], ")"))]); 34 | d.accept(this); 35 | } 36 | 37 | private enum string KEY = "dscanner.deprecated.delete_keyword"; 38 | } 39 | 40 | unittest 41 | { 42 | import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; 43 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 44 | 45 | StaticAnalysisConfig sac = disabledConfig(); 46 | sac.delete_check = Check.enabled; 47 | assertAnalyzerWarnings(q{ 48 | void testDelete() 49 | { 50 | int[int] data = [1 : 2]; 51 | delete data[1]; /+ 52 | ^^^^^^ [warn]: Avoid using the 'delete' keyword. +/ 53 | 54 | auto a = new Class(); 55 | delete a; /+ 56 | ^^^^^^ [warn]: Avoid using the 'delete' keyword. +/ 57 | } 58 | }c, sac); 59 | 60 | assertAutoFix(q{ 61 | void testDelete() 62 | { 63 | int[int] data = [1 : 2]; 64 | delete data[1]; // fix 65 | 66 | auto a = new Class(); 67 | delete a; // fix 68 | } 69 | }c, q{ 70 | void testDelete() 71 | { 72 | int[int] data = [1 : 2]; 73 | destroy(data[1]); // fix 74 | 75 | auto a = new Class(); 76 | destroy(a); // fix 77 | } 78 | }c, sac); 79 | 80 | stderr.writeln("Unittest for DeleteCheck passed."); 81 | } 82 | -------------------------------------------------------------------------------- /src/dscanner/analysis/logic_precedence.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.logic_precedence; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_; 14 | 15 | /** 16 | * Checks for code with confusing && and || operator precedence 17 | * --- 18 | * if (a && b || c) // bad 19 | * if (a && (b || c)) // good 20 | * --- 21 | */ 22 | final class LogicPrecedenceCheck : BaseAnalyzer 23 | { 24 | alias visit = BaseAnalyzer.visit; 25 | 26 | enum string KEY = "dscanner.confusing.logical_precedence"; 27 | mixin AnalyzerInfo!"logical_precedence_check"; 28 | 29 | this(BaseAnalyzerArguments args) 30 | { 31 | super(args); 32 | } 33 | 34 | override void visit(const OrOrExpression orOr) 35 | { 36 | if (orOr.left is null || orOr.right is null) 37 | return; 38 | const AndAndExpression left = cast(AndAndExpression) orOr.left; 39 | const AndAndExpression right = cast(AndAndExpression) orOr.right; 40 | if (left is null && right is null) 41 | return; 42 | if ((left !is null && left.right is null) && (right !is null && right.right is null)) 43 | return; 44 | addErrorMessage(orOr, KEY, 45 | "Use parenthesis to clarify this expression."); 46 | orOr.accept(this); 47 | } 48 | } 49 | 50 | unittest 51 | { 52 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 53 | 54 | StaticAnalysisConfig sac = disabledConfig(); 55 | sac.logical_precedence_check = Check.enabled; 56 | assertAnalyzerWarnings(q{ 57 | void testFish() 58 | { 59 | if (a && b || c) {} /+ 60 | ^^^^^^^^^^^ [warn]: Use parenthesis to clarify this expression. +/ 61 | if ((a && b) || c) {} // Good 62 | if (b || c && d) {} /+ 63 | ^^^^^^^^^^^ [warn]: Use parenthesis to clarify this expression. +/ 64 | if (b || (c && d)) {} // Good 65 | } 66 | }c, sac); 67 | stderr.writeln("Unittest for LogicPrecedenceCheck passed."); 68 | } 69 | -------------------------------------------------------------------------------- /tests/it/autofix_ide/source_autofix.report.json: -------------------------------------------------------------------------------- 1 | { 2 | "classCount": 0, 3 | "functionCount": 1, 4 | "interfaceCount": 0, 5 | "issues": [ 6 | { 7 | "column": 6, 8 | "endColumn": 12, 9 | "endIndex": 22, 10 | "endLine": 3, 11 | "fileName": "it/autofix_ide/source_autofix.d", 12 | "index": 16, 13 | "key": "dscanner.confusing.function_attributes", 14 | "line": 3, 15 | "message": "Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'.", 16 | "name": "function_attribute_check", 17 | "supplemental": [], 18 | "type": "warn", 19 | "autofixes": [ 20 | { 21 | "name": "Mark function `const`", 22 | "replacements": [ 23 | { 24 | "newText": " const", 25 | "range": [ 26 | 24, 27 | 24 28 | ] 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "Mark function `inout`", 34 | "replacements": [ 35 | { 36 | "newText": " inout", 37 | "range": [ 38 | 24, 39 | 24 40 | ] 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "Mark function `immutable`", 46 | "replacements": [ 47 | { 48 | "newText": " immutable", 49 | "range": [ 50 | 24, 51 | 24 52 | ] 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | { 59 | "autofixes": [ 60 | { 61 | "name": "Insert `static`", 62 | "replacements": [ 63 | { 64 | "newText": "static ", 65 | "range": [ 66 | 69, 67 | 69 68 | ] 69 | } 70 | ] 71 | }, 72 | { 73 | "name": "Wrap '{}' block around 'if'", 74 | "replacements": "resolvable" 75 | } 76 | ], 77 | "column": 3, 78 | "endColumn": 10, 79 | "endIndex": 71, 80 | "endLine": 8, 81 | "fileName": "it/autofix_ide/source_autofix.d", 82 | "index": 64, 83 | "key": "dscanner.suspicious.static_if_else", 84 | "line": 8, 85 | "message": "Mismatched static if. Use 'else static if' here.", 86 | "name": "static_if_else_check", 87 | "supplemental": [], 88 | "type": "warn" 89 | } 90 | ], 91 | "lineOfCodeCount": 3, 92 | "statementCount": 4, 93 | "structCount": 1, 94 | "templateCount": 0, 95 | "undocumentedPublicSymbols": 0 96 | } -------------------------------------------------------------------------------- /setup-ldc-windows.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # sets up LDC for cross-compilation. Source this script, s.t. the new LDC is in PATH 4 | 5 | LDC_VERSION="1.13.0" 6 | ARCH=${ARCH:-32} 7 | VERSION=$(git describe --abbrev=0 --tags) 8 | OS=windows 9 | 10 | # Step 0: install ldc 11 | if [ ! -f install.sh ] ; then 12 | wget https://dlang.org/install.sh 13 | fi 14 | . $(bash ./install.sh -a "ldc-${LDC_VERSION}") 15 | 16 | # for the install.sh script only 17 | LDC_PATH="$(dirname $(dirname $(which ldc2)))" 18 | 19 | # Step 1a: download the LDC x64 windows binaries 20 | if [ "${ARCH}" == 64 ] && [ ! -d "ldc2-${LDC_VERSION}-windows-x64" ] ; then 21 | wget "https://github.com/ldc-developers/ldc/releases/download/v1.13.0/ldc2-${LDC_VERSION}-windows-x64.7z" 22 | 7z x "ldc2-${LDC_VERSION}-windows-x64.7z" > /dev/null 23 | # Step 2a: Add LDC windows binaries to LDC Linux 24 | if [ ! -d "${LDC_PATH}/lib-win64" ] ; then 25 | cp -r ldc2-1.13.0-windows-x64/lib "${LDC_PATH}/lib-win64" 26 | cat >> "$LDC_PATH"/etc/ldc2.conf < /dev/null 44 | # Step 2b: Add LDC windows binaries to LDC Linux 45 | if [ ! -d "${LDC_PATH}/lib-win32" ] ; then 46 | cp -r ldc2-1.13.0-windows-x86/lib "${LDC_PATH}/lib-win32" 47 | cat >> "$LDC_PATH"/etc/ldc2.conf <" || r.operator == tok!"<>=" 33 | || r.operator == tok!"!<>" || r.operator == tok!"!>" 34 | || r.operator == tok!"!<" || r.operator == tok!"!<>=" 35 | || r.operator == tok!"!>=" || r.operator == tok!"!<=") 36 | { 37 | addErrorMessage(r, KEY, 38 | "Avoid using the deprecated floating-point operators."); 39 | } 40 | r.accept(this); 41 | } 42 | } 43 | 44 | unittest 45 | { 46 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 47 | 48 | StaticAnalysisConfig sac = disabledConfig(); 49 | sac.float_operator_check = Check.enabled; 50 | assertAnalyzerWarnings(q{ 51 | void testFish() 52 | { 53 | float z = 1.5f; 54 | bool a; 55 | a = z !<>= z; /+ 56 | ^^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 57 | a = z !<> z; /+ 58 | ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 59 | a = z <> z; /+ 60 | ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 61 | a = z <>= z; /+ 62 | ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 63 | a = z !> z; /+ 64 | ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 65 | a = z !>= z; /+ 66 | ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 67 | a = z !< z; /+ 68 | ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 69 | a = z !<= z; /+ 70 | ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ 71 | } 72 | }c, sac); 73 | 74 | stderr.writeln("Unittest for FloatOperatorCheck passed."); 75 | } 76 | -------------------------------------------------------------------------------- /src/dscanner/analysis/length_subtraction.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.length_subtraction; 7 | 8 | import std.stdio; 9 | 10 | import dparse.ast; 11 | import dparse.lexer; 12 | import dscanner.analysis.base; 13 | import dscanner.analysis.helpers; 14 | import dsymbol.scope_; 15 | 16 | /** 17 | * Checks for subtraction from a .length property. This is usually a bug. 18 | */ 19 | final class LengthSubtractionCheck : BaseAnalyzer 20 | { 21 | private enum string KEY = "dscanner.suspicious.length_subtraction"; 22 | 23 | alias visit = BaseAnalyzer.visit; 24 | 25 | mixin AnalyzerInfo!"length_subtraction_check"; 26 | 27 | this(BaseAnalyzerArguments args) 28 | { 29 | super(args); 30 | } 31 | 32 | override void visit(const AddExpression addExpression) 33 | { 34 | if (addExpression.operator == tok!"-") 35 | { 36 | const UnaryExpression l = cast(const UnaryExpression) addExpression.left; 37 | const UnaryExpression r = cast(const UnaryExpression) addExpression.right; 38 | if (l is null || r is null) 39 | goto end; 40 | if (r.primaryExpression is null || r.primaryExpression.primary.type != tok!"intLiteral") 41 | goto end; 42 | if (l.identifierOrTemplateInstance is null 43 | || l.identifierOrTemplateInstance.identifier.text != "length") 44 | goto end; 45 | addErrorMessage(addExpression, KEY, 46 | "Avoid subtracting from '.length' as it may be unsigned.", 47 | [ 48 | AutoFix.insertionBefore(l.tokens[0], "cast(ptrdiff_t) ", "Cast to ptrdiff_t") 49 | ]); 50 | } 51 | end: 52 | addExpression.accept(this); 53 | } 54 | } 55 | 56 | unittest 57 | { 58 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 59 | 60 | StaticAnalysisConfig sac = disabledConfig(); 61 | sac.length_subtraction_check = Check.enabled; 62 | assertAnalyzerWarnings(q{ 63 | void testSizeT() 64 | { 65 | if (i < a.length - 1) /+ 66 | ^^^^^^^^^^^^ [warn]: Avoid subtracting from '.length' as it may be unsigned. +/ 67 | writeln("something"); 68 | } 69 | }c, sac); 70 | 71 | assertAutoFix(q{ 72 | void testSizeT() 73 | { 74 | if (i < a.length - 1) // fix 75 | writeln("something"); 76 | } 77 | }c, q{ 78 | void testSizeT() 79 | { 80 | if (i < cast(ptrdiff_t) a.length - 1) // fix 81 | writeln("something"); 82 | } 83 | }c, sac); 84 | stderr.writeln("Unittest for IfElseSameCheck passed."); 85 | } 86 | -------------------------------------------------------------------------------- /scripts/uml.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | digraph { 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/dscanner/analysis/enumarrayliteral.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.enumarrayliteral; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import std.algorithm : find, map; 12 | import dsymbol.scope_ : Scope; 13 | 14 | void doNothing(string, size_t, size_t, string, bool) 15 | { 16 | } 17 | 18 | final class EnumArrayLiteralCheck : BaseAnalyzer 19 | { 20 | alias visit = BaseAnalyzer.visit; 21 | 22 | mixin AnalyzerInfo!"enum_array_literal_check"; 23 | 24 | this(BaseAnalyzerArguments args) 25 | { 26 | super(args); 27 | } 28 | 29 | bool looking; 30 | 31 | mixin visitTemplate!ClassDeclaration; 32 | mixin visitTemplate!InterfaceDeclaration; 33 | mixin visitTemplate!UnionDeclaration; 34 | mixin visitTemplate!StructDeclaration; 35 | 36 | override void visit(const AutoDeclaration autoDec) 37 | { 38 | auto enumToken = autoDec.storageClasses.find!(a => a.token == tok!"enum"); 39 | if (enumToken.length) 40 | { 41 | foreach (part; autoDec.parts) 42 | { 43 | if (part.initializer is null) 44 | continue; 45 | if (part.initializer.nonVoidInitializer is null) 46 | continue; 47 | if (part.initializer.nonVoidInitializer.arrayInitializer is null) 48 | continue; 49 | addErrorMessage(part.initializer.nonVoidInitializer, 50 | KEY, 51 | "This enum may lead to unnecessary allocation at run-time." 52 | ~ " Use 'static immutable " 53 | ~ part.identifier.text ~ " = [ ...' instead.", 54 | [ 55 | AutoFix.replacement(enumToken[0].token, "static immutable") 56 | ]); 57 | } 58 | } 59 | autoDec.accept(this); 60 | } 61 | 62 | private enum string KEY = "dscanner.performance.enum_array_literal"; 63 | } 64 | 65 | unittest 66 | { 67 | import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; 68 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 69 | import std.stdio : stderr; 70 | 71 | StaticAnalysisConfig sac = disabledConfig(); 72 | sac.enum_array_literal_check = Check.enabled; 73 | assertAnalyzerWarnings(q{ 74 | enum x = [1, 2, 3]; /+ 75 | ^^^^^^^^^ [warn]: This enum may lead to unnecessary allocation at run-time. Use 'static immutable x = [ ...' instead. +/ 76 | }c, sac); 77 | 78 | assertAutoFix(q{ 79 | enum x = [1, 2, 3]; // fix 80 | }c, q{ 81 | static immutable x = [1, 2, 3]; // fix 82 | }c, sac); 83 | 84 | stderr.writeln("Unittest for EnumArrayLiteralCheck passed."); 85 | } 86 | -------------------------------------------------------------------------------- /src/dscanner/analysis/local_imports.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.local_imports; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_; 14 | 15 | /** 16 | * Checks for local imports that import all symbols. 17 | * See_also: $(LINK https://issues.dlang.org/show_bug.cgi?id=10378) 18 | */ 19 | final class LocalImportCheck : BaseAnalyzer 20 | { 21 | alias visit = BaseAnalyzer.visit; 22 | 23 | mixin AnalyzerInfo!"local_import_check"; 24 | 25 | /** 26 | * Construct with the given file name. 27 | */ 28 | this(BaseAnalyzerArguments args) 29 | { 30 | super(args); 31 | } 32 | 33 | mixin visitThing!StructBody; 34 | mixin visitThing!BlockStatement; 35 | 36 | override void visit(const Declaration dec) 37 | { 38 | if (dec.importDeclaration is null) 39 | { 40 | dec.accept(this); 41 | return; 42 | } 43 | foreach (attr; dec.attributes) 44 | { 45 | if (attr.attribute == tok!"static") 46 | isStatic = true; 47 | } 48 | dec.accept(this); 49 | isStatic = false; 50 | } 51 | 52 | override void visit(const ImportDeclaration id) 53 | { 54 | if ((!isStatic && interesting) && (id.importBindings is null 55 | || id.importBindings.importBinds.length == 0)) 56 | { 57 | foreach (singleImport; id.singleImports) 58 | { 59 | if (singleImport.rename.text.length == 0) 60 | { 61 | addErrorMessage(singleImport, 62 | KEY, "Local imports should specify" 63 | ~ " the symbols being imported to avoid hiding local symbols."); 64 | } 65 | } 66 | } 67 | } 68 | 69 | private: 70 | 71 | enum string KEY = "dscanner.suspicious.local_imports"; 72 | 73 | mixin template visitThing(T) 74 | { 75 | override void visit(const T thing) 76 | { 77 | const b = interesting; 78 | interesting = true; 79 | thing.accept(this); 80 | interesting = b; 81 | } 82 | } 83 | 84 | bool interesting; 85 | bool isStatic; 86 | } 87 | 88 | unittest 89 | { 90 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 91 | 92 | StaticAnalysisConfig sac = disabledConfig(); 93 | sac.local_import_check = Check.enabled; 94 | assertAnalyzerWarnings(q{ 95 | void testLocalImport() 96 | { 97 | import std.stdio; /+ 98 | ^^^^^^^^^ [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. +/ 99 | import std.fish : scales, head; 100 | import DAGRON = std.experimental.dragon; 101 | } 102 | }c, sac); 103 | 104 | stderr.writeln("Unittest for LocalImportCheck passed."); 105 | } 106 | -------------------------------------------------------------------------------- /tests/it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu -o pipefail 4 | 5 | function section { 6 | e=$'\e' 7 | if [ ! -z "${GITHUB_ACTION:-}" ]; then 8 | echo "::endgroup::" 9 | echo "::group::$@" 10 | else 11 | echo "$e[1m$@$e[m" 12 | fi 13 | } 14 | 15 | function error { 16 | echo $'\e[31;1mTests have failed.\e[m' 17 | exit 1 18 | } 19 | 20 | function cleanup { 21 | if [ ! -z "${GITHUB_ACTION:-}" ]; then 22 | echo "::endgroup::" 23 | fi 24 | } 25 | 26 | DSCANNER_DIR="$(dirname -- $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ))" 27 | 28 | if [ ! -z "${GITHUB_ACTION:-}" ]; then 29 | echo "::group::Building d-scanner" 30 | fi 31 | 32 | trap cleanup EXIT 33 | trap error ERR 34 | 35 | if [ -z "${CI:-}" ]; then 36 | dub build --root="$DSCANNER_DIR" 37 | fi 38 | 39 | cd "$DSCANNER_DIR/tests" 40 | 41 | # IDE APIs 42 | # -------- 43 | # checking that reporting format stays consistent or only gets extended 44 | diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json) 45 | diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json) 46 | 47 | # CLI tests 48 | # --------- 49 | # check that `dscanner fix` works as expected 50 | section '1. test no changes if EOFing' 51 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 52 | printf "" | ../bin/dscanner fix it/autofix_cli/test.d 53 | diff it/autofix_cli/test.d it/autofix_cli/source.d 54 | section '2. test no changes for simple enter pressing' 55 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 56 | printf "\n" | ../bin/dscanner fix it/autofix_cli/test.d 57 | diff it/autofix_cli/test.d it/autofix_cli/source.d 58 | section '2.1. test no changes entering 0' 59 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 60 | printf "0\n" | ../bin/dscanner fix it/autofix_cli/test.d 61 | diff it/autofix_cli/test.d it/autofix_cli/source.d 62 | section '3. test change applies automatically with --applySingle' 63 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 64 | ../bin/dscanner fix --applySingle it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d' 65 | diff it/autofix_cli/test.d it/autofix_cli/fixed.d 66 | section '4. test change apply when entering "1"' 67 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 68 | printf "1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d' 69 | diff it/autofix_cli/test.d it/autofix_cli/fixed.d 70 | section '5. test invalid selection reasks what to apply' 71 | cp -v it/autofix_cli/source.d it/autofix_cli/test.d 72 | printf "2\n-1\n1000\na\n1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d' 73 | diff it/autofix_cli/test.d it/autofix_cli/fixed.d 74 | 75 | # check that `dscanner @myargs.rst` reads arguments from file 76 | section "Test @myargs.rst" 77 | echo "-f" > "myargs.rst" 78 | echo "github" >> "myargs.rst" 79 | echo "lint" >> "myargs.rst" 80 | echo "it/singleissue.d" >> "myargs.rst" 81 | diff it/singleissue_github.txt <(../bin/dscanner "@myargs.rst") 82 | rm "myargs.rst" 83 | -------------------------------------------------------------------------------- /src/dscanner/analysis/unused_variable.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014-2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | module dscanner.analysis.unused_variable; 6 | 7 | import dparse.ast; 8 | import dscanner.analysis.base; 9 | import dscanner.analysis.unused; 10 | import dsymbol.scope_ : Scope; 11 | import std.algorithm.iteration : map; 12 | 13 | /** 14 | * Checks for unused variables. 15 | */ 16 | final class UnusedVariableCheck : UnusedStorageCheck 17 | { 18 | alias visit = UnusedStorageCheck.visit; 19 | 20 | mixin AnalyzerInfo!"unused_variable_check"; 21 | 22 | /** 23 | * Params: 24 | * fileName = the name of the file being analyzed 25 | */ 26 | this(BaseAnalyzerArguments args) 27 | { 28 | super(args, "Variable", "unused_variable"); 29 | } 30 | 31 | override void visit(const VariableDeclaration variableDeclaration) 32 | { 33 | foreach (d; variableDeclaration.declarators) 34 | this.variableDeclared(d.name.text, d.name, false); 35 | variableDeclaration.accept(this); 36 | } 37 | 38 | override void visit(const AutoDeclaration autoDeclaration) 39 | { 40 | foreach (t; autoDeclaration.parts.map!(a => a.identifier)) 41 | this.variableDeclared(t.text, t, false); 42 | autoDeclaration.accept(this); 43 | } 44 | } 45 | 46 | @system unittest 47 | { 48 | import std.stdio : stderr; 49 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 50 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 51 | 52 | StaticAnalysisConfig sac = disabledConfig(); 53 | sac.unused_variable_check = Check.enabled; 54 | assertAnalyzerWarnings(q{ 55 | 56 | // Issue 274 57 | unittest 58 | { 59 | size_t byteIndex = 0; 60 | *(cast(FieldType*)(retVal.ptr + byteIndex)) = item; 61 | } 62 | 63 | unittest 64 | { 65 | int a; /+ 66 | ^ [warn]: Variable a is never used. +/ 67 | } 68 | 69 | // Issue 380 70 | int templatedEnum() 71 | { 72 | enum a(T) = T.init; 73 | return a!int; 74 | } 75 | 76 | // Issue 380 77 | int otherTemplatedEnum() 78 | { 79 | auto a(T) = T.init; /+ 80 | ^ [warn]: Variable a is never used. +/ 81 | return 0; 82 | } 83 | 84 | // Issue 364 85 | void test364_1() 86 | { 87 | enum s = 8; 88 | immutable t = 2; 89 | int[s][t] a; 90 | a[0][0] = 1; 91 | } 92 | 93 | void test364_2() 94 | { 95 | enum s = 8; 96 | alias a = e!s; 97 | a = 1; 98 | } 99 | 100 | void oops () 101 | { 102 | class Identity { int val; } 103 | Identity v; 104 | v.val = 0; 105 | } 106 | 107 | void main() 108 | { 109 | const int testValue; 110 | testValue.writeln; 111 | } 112 | 113 | // Issue 788 114 | void traits() 115 | { 116 | enum fieldName = "abc"; 117 | __traits(hasMember, S, fieldName); 118 | 119 | __traits(compiles, { int i = 2; }); 120 | } 121 | 122 | // segfault with null templateArgumentList 123 | void nullTest() 124 | { 125 | __traits(isPOD); 126 | } 127 | 128 | void unitthreaded() 129 | { 130 | auto testVar = foo.sort!myComp; 131 | genVar.should == testVar; 132 | } 133 | 134 | }c, sac); 135 | stderr.writeln("Unittest for UnusedVariableCheck passed."); 136 | } 137 | -------------------------------------------------------------------------------- /src/dscanner/analysis/pokemon.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.pokemon; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_ : Scope; 14 | 15 | /** 16 | * Checks for Pokémon exception handling, i.e. "gotta' catch 'em all". 17 | * 18 | * --- 19 | * try { 20 | * choose(pikachu); 21 | * } catch (Throwable e) { 22 | * ... 23 | * } 24 | * --- 25 | */ 26 | final class PokemonExceptionCheck : BaseAnalyzer 27 | { 28 | enum MESSAGE = "Catching Error or Throwable is almost always a bad idea."; 29 | enum string KEY = "dscanner.suspicious.catch_em_all"; 30 | mixin AnalyzerInfo!"exception_check"; 31 | 32 | alias visit = BaseAnalyzer.visit; 33 | 34 | this(BaseAnalyzerArguments args) 35 | { 36 | super(args); 37 | } 38 | 39 | override void visit(const LastCatch lc) 40 | { 41 | addErrorMessage(lc.tokens[0], KEY, MESSAGE); 42 | lc.accept(this); 43 | } 44 | 45 | bool ignoreType = true; 46 | 47 | override void visit(const Catch c) 48 | { 49 | ignoreType = false; 50 | c.type.accept(this); 51 | ignoreType = true; 52 | 53 | c.accept(this); 54 | } 55 | 56 | override void visit(const Type2 type2) 57 | { 58 | if (ignoreType) 59 | return; 60 | 61 | if (type2.type !is null) 62 | { 63 | type2.type.accept(this); 64 | return; 65 | } 66 | 67 | if (type2.typeIdentifierPart.typeIdentifierPart !is null) 68 | { 69 | return; 70 | } 71 | const identOrTemplate = type2.typeIdentifierPart.identifierOrTemplateInstance; 72 | if (identOrTemplate.templateInstance !is null) 73 | { 74 | return; 75 | } 76 | if (identOrTemplate.identifier.text == "Throwable" 77 | || identOrTemplate.identifier.text == "Error") 78 | { 79 | addErrorMessage(identOrTemplate, KEY, MESSAGE); 80 | } 81 | } 82 | } 83 | 84 | unittest 85 | { 86 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 87 | 88 | StaticAnalysisConfig sac = disabledConfig(); 89 | sac.exception_check = Check.enabled; 90 | assertAnalyzerWarnings(q{ 91 | void testCatch() 92 | { 93 | try 94 | { 95 | // ... 96 | } 97 | catch (AssertError err) //ok 98 | { 99 | 100 | } 101 | catch (Exception err) // ok 102 | { 103 | 104 | } 105 | catch (shared(Exception) err) // ok 106 | { 107 | 108 | } 109 | catch (Error err) /+ 110 | ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ 111 | { 112 | 113 | } 114 | catch (Throwable err) /+ 115 | ^^^^^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ 116 | { 117 | 118 | } 119 | catch (shared(Error) err) /+ 120 | ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ 121 | { 122 | 123 | } 124 | catch /+ 125 | ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ 126 | { 127 | 128 | } 129 | } 130 | }c, sac); 131 | 132 | stderr.writeln("Unittest for PokemonExceptionCheck passed."); 133 | } 134 | -------------------------------------------------------------------------------- /src/dscanner/analysis/unused_parameter.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014-2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | module dscanner.analysis.unused_parameter; 6 | 7 | import dparse.ast; 8 | import dparse.lexer; 9 | import dscanner.analysis.base; 10 | import dscanner.analysis.unused; 11 | import dsymbol.scope_ : Scope; 12 | 13 | /** 14 | * Checks for unused variables. 15 | */ 16 | final class UnusedParameterCheck : UnusedStorageCheck 17 | { 18 | alias visit = UnusedStorageCheck.visit; 19 | 20 | mixin AnalyzerInfo!"unused_parameter_check"; 21 | 22 | /** 23 | * Params: 24 | * fileName = the name of the file being analyzed 25 | */ 26 | this(BaseAnalyzerArguments args) 27 | { 28 | super(args, "Parameter", "unused_parameter"); 29 | } 30 | 31 | override void visit(const Parameter parameter) 32 | { 33 | import std.algorithm : among; 34 | import std.algorithm.iteration : filter; 35 | import std.range : empty; 36 | 37 | if (parameter.name != tok!"") 38 | { 39 | immutable bool isRef = !parameter.parameterAttributes 40 | .filter!(a => a.idType.among(tok!"ref", tok!"out")).empty; 41 | immutable bool isPtr = parameter.type && !parameter.type 42 | .typeSuffixes.filter!(a => a.star != tok!"").empty; 43 | 44 | variableDeclared(parameter.name.text, parameter.name, isRef | isPtr); 45 | 46 | if (parameter.default_ !is null) 47 | { 48 | interestDepth++; 49 | parameter.default_.accept(this); 50 | interestDepth--; 51 | } 52 | } 53 | } 54 | } 55 | 56 | @system unittest 57 | { 58 | import std.stdio : stderr; 59 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 60 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 61 | 62 | StaticAnalysisConfig sac = disabledConfig(); 63 | sac.unused_parameter_check = Check.enabled; 64 | assertAnalyzerWarnings(q{ 65 | 66 | // bug encountered after correct DIP 1009 impl in dparse 67 | version (StdDdoc) 68 | { 69 | bool isAbsolute(R)(R path) pure nothrow @safe 70 | if (isRandomAccessRange!R && isSomeChar!(ElementType!R) || 71 | is(StringTypeOf!R)); 72 | } 73 | 74 | void inPSC(in int a){} /+ 75 | ^ [warn]: Parameter a is never used. +/ 76 | 77 | void doStuff(int a, int b) /+ 78 | ^ [warn]: Parameter b is never used. +/ 79 | { 80 | return a; 81 | } 82 | 83 | // Issue 352 84 | void test352_1() 85 | { 86 | void f(int *x) {*x = 1;} 87 | } 88 | 89 | void test352_2() 90 | { 91 | void f(Bat** bat) {*bat = bats.ptr + 8;} 92 | } 93 | 94 | // Issue 490 95 | void test490() 96 | { 97 | auto cb1 = delegate(size_t _) {}; 98 | cb1(3); 99 | auto cb2 = delegate(size_t a) {}; /+ 100 | ^ [warn]: Parameter a is never used. +/ 101 | cb2(3); 102 | } 103 | 104 | bool hasDittos(int decl) 105 | { 106 | mixin("decl++;"); 107 | } 108 | 109 | // https://github.com/dlang-community/D-Scanner/issues/794 110 | void traits() 111 | { 112 | struct S { int i; } 113 | 114 | static foo(S s) 115 | { 116 | __traits(getMember, s, "i") = 99; 117 | } 118 | } 119 | 120 | }c, sac); 121 | stderr.writeln("Unittest for UnusedParameterCheck passed."); 122 | } 123 | -------------------------------------------------------------------------------- /src/dscanner/analysis/builtin_property_names.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.builtin_property_names; 7 | 8 | import std.stdio; 9 | import std.regex; 10 | import dparse.ast; 11 | import dparse.lexer; 12 | import dscanner.analysis.base; 13 | import dscanner.analysis.helpers; 14 | import dsymbol.scope_; 15 | import std.algorithm : map; 16 | 17 | /** 18 | * The following code should be killed with fire: 19 | * --- 20 | * class SomeClass 21 | * { 22 | * void init(); 23 | * int init; 24 | * string mangleof = "LOL"; 25 | * auto init = 10; 26 | * enum sizeof = 10; 27 | * } 28 | * --- 29 | */ 30 | final class BuiltinPropertyNameCheck : BaseAnalyzer 31 | { 32 | alias visit = BaseAnalyzer.visit; 33 | 34 | mixin AnalyzerInfo!"builtin_property_names_check"; 35 | 36 | this(BaseAnalyzerArguments args) 37 | { 38 | super(args); 39 | } 40 | 41 | override void visit(const FunctionDeclaration fd) 42 | { 43 | if (depth > 0 && isBuiltinProperty(fd.name.text)) 44 | { 45 | addErrorMessage(fd.name, KEY, generateErrorMessage(fd.name.text)); 46 | } 47 | fd.accept(this); 48 | } 49 | 50 | override void visit(const FunctionBody functionBody) 51 | { 52 | immutable int d = depth; 53 | scope (exit) 54 | depth = d; 55 | depth = 0; 56 | functionBody.accept(this); 57 | } 58 | 59 | override void visit(const AutoDeclaration ad) 60 | { 61 | if (depth > 0) 62 | foreach (i; ad.parts.map!(a => a.identifier)) 63 | { 64 | if (isBuiltinProperty(i.text)) 65 | addErrorMessage(i, KEY, generateErrorMessage(i.text)); 66 | } 67 | } 68 | 69 | override void visit(const Declarator d) 70 | { 71 | if (depth > 0 && isBuiltinProperty(d.name.text)) 72 | addErrorMessage(d.name, KEY, generateErrorMessage(d.name.text)); 73 | } 74 | 75 | override void visit(const StructBody sb) 76 | { 77 | depth++; 78 | sb.accept(this); 79 | depth--; 80 | } 81 | 82 | private: 83 | 84 | enum string KEY = "dscanner.confusing.builtin_property_names"; 85 | 86 | string generateErrorMessage(string name) 87 | { 88 | import std.string : format; 89 | 90 | return format("Avoid naming members '%s'. This can" 91 | ~ " confuse code that depends on the '.%s' property of a type.", name, name); 92 | } 93 | 94 | bool isBuiltinProperty(string name) 95 | { 96 | import std.algorithm : canFind; 97 | 98 | return BuiltinProperties.canFind(name); 99 | } 100 | 101 | enum string[] BuiltinProperties = ["init", "sizeof", "mangleof", "alignof", "stringof"]; 102 | int depth; 103 | } 104 | 105 | unittest 106 | { 107 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 108 | 109 | StaticAnalysisConfig sac = disabledConfig(); 110 | sac.builtin_property_names_check = Check.enabled; 111 | assertAnalyzerWarnings(q{ 112 | class SomeClass 113 | { 114 | void init(); /+ 115 | ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ 116 | int init; /+ 117 | ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ 118 | auto init = 10; /+ 119 | ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ 120 | } 121 | }c, sac); 122 | 123 | stderr.writeln("Unittest for NumberStyleCheck passed."); 124 | } 125 | -------------------------------------------------------------------------------- /src/dscanner/analysis/auto_ref_assignment.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.auto_ref_assignment; 7 | 8 | import dparse.lexer; 9 | import dparse.ast; 10 | import dscanner.analysis.base; 11 | 12 | /** 13 | * Checks for assignment to auto-ref function parameters. 14 | */ 15 | final class AutoRefAssignmentCheck : BaseAnalyzer 16 | { 17 | mixin AnalyzerInfo!"auto_ref_assignment_check"; 18 | 19 | /// 20 | this(BaseAnalyzerArguments args) 21 | { 22 | super(args); 23 | } 24 | 25 | override void visit(const Module m) 26 | { 27 | pushScope(); 28 | m.accept(this); 29 | popScope(); 30 | } 31 | 32 | override void visit(const FunctionDeclaration func) 33 | { 34 | if (func.parameters is null || func.parameters.parameters.length == 0) 35 | return; 36 | pushScope(); 37 | scope (exit) 38 | popScope(); 39 | func.accept(this); 40 | } 41 | 42 | override void visit(const Parameter param) 43 | { 44 | import std.algorithm.searching : canFind; 45 | 46 | immutable bool isAuto = param.parameterAttributes.canFind!(a => a.idType == cast(ubyte) tok!"auto"); 47 | immutable bool isRef = param.parameterAttributes.canFind!(a => a.idType == cast(ubyte) tok!"ref"); 48 | if (!isAuto || !isRef) 49 | return; 50 | addSymbol(param.name.text); 51 | } 52 | 53 | override void visit(const AssignExpression assign) 54 | { 55 | if (assign.operator == tok!"" || scopes.length == 0) 56 | return; 57 | interest ~= assign; 58 | assign.ternaryExpression.accept(this); 59 | interest.length--; 60 | } 61 | 62 | override void visit(const IdentifierOrTemplateInstance ioti) 63 | { 64 | import std.algorithm.searching : canFind; 65 | 66 | if (ioti.identifier == tok!"" || !interest.length) 67 | return; 68 | if (scopes[$ - 1].canFind(ioti.identifier.text)) 69 | addErrorMessage(interest[$ - 1], KEY, MESSAGE); 70 | } 71 | 72 | override void visit(const IdentifierChain ic) 73 | { 74 | import std.algorithm.searching : canFind; 75 | 76 | if (ic.identifiers.length == 0 || !interest.length) 77 | return; 78 | if (scopes[$ - 1].canFind(ic.identifiers[0].text)) 79 | addErrorMessage(interest[$ - 1], KEY, MESSAGE); 80 | } 81 | 82 | alias visit = BaseAnalyzer.visit; 83 | 84 | private: 85 | 86 | enum string MESSAGE = "Assignment to auto-ref function parameter."; 87 | enum string KEY = "dscanner.suspicious.auto_ref_assignment"; 88 | 89 | const(AssignExpression)[] interest; 90 | 91 | void addSymbol(string symbolName) 92 | { 93 | scopes[$ - 1] ~= symbolName; 94 | } 95 | 96 | void pushScope() 97 | { 98 | scopes.length++; 99 | } 100 | 101 | void popScope() 102 | { 103 | scopes = scopes[0 .. $ - 1]; 104 | } 105 | 106 | string[][] scopes; 107 | } 108 | 109 | unittest 110 | { 111 | import std.stdio : stderr; 112 | import std.format : format; 113 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 114 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 115 | 116 | StaticAnalysisConfig sac = disabledConfig(); 117 | sac.auto_ref_assignment_check = Check.enabled; 118 | assertAnalyzerWarnings(q{ 119 | int doStuff(T)(auto ref int a) 120 | { 121 | a = 10; /+ 122 | ^^^^^^ [warn]: %s +/ 123 | } 124 | 125 | int doStuff(T)(ref int a) 126 | { 127 | a = 10; 128 | } 129 | }c.format(AutoRefAssignmentCheck.MESSAGE), sac); 130 | stderr.writeln("Unittest for AutoRefAssignmentCheck passed."); 131 | } 132 | -------------------------------------------------------------------------------- /src/dscanner/analysis/explicitly_annotated_unittests.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.explicitly_annotated_unittests; 6 | 7 | import dparse.lexer; 8 | import dparse.ast; 9 | import dscanner.analysis.base; 10 | 11 | import std.stdio; 12 | 13 | /** 14 | * Requires unittests to be explicitly annotated with either @safe or @system 15 | */ 16 | final class ExplicitlyAnnotatedUnittestCheck : BaseAnalyzer 17 | { 18 | enum string KEY = "dscanner.style.explicitly_annotated_unittest"; 19 | enum string MESSAGE = "A unittest should be annotated with at least @safe or @system"; 20 | mixin AnalyzerInfo!"explicitly_annotated_unittests"; 21 | 22 | /// 23 | this(BaseAnalyzerArguments args) 24 | { 25 | super(args); 26 | } 27 | 28 | override void visit(const Declaration decl) 29 | { 30 | if (decl.unittest_ !is null) 31 | { 32 | bool isSafeOrSystem; 33 | if (decl.attributes !is null) 34 | foreach (attribute; decl.attributes) 35 | { 36 | if (attribute.atAttribute !is null) 37 | { 38 | const token = attribute.atAttribute.identifier.text; 39 | if (token == "safe" || token == "system") 40 | { 41 | isSafeOrSystem = true; 42 | break; 43 | } 44 | } 45 | } 46 | if (!isSafeOrSystem) 47 | { 48 | auto token = decl.unittest_.findTokenForDisplay(tok!"unittest"); 49 | addErrorMessage(token, KEY, MESSAGE, 50 | [ 51 | AutoFix.insertionBefore(token[0], "@safe ", "Mark unittest @safe"), 52 | AutoFix.insertionBefore(token[0], "@system ", "Mark unittest @system") 53 | ]); 54 | } 55 | } 56 | decl.accept(this); 57 | } 58 | 59 | alias visit = BaseAnalyzer.visit; 60 | 61 | } 62 | 63 | unittest 64 | { 65 | import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; 66 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 67 | import std.format : format; 68 | import std.stdio : stderr; 69 | 70 | StaticAnalysisConfig sac = disabledConfig(); 71 | sac.explicitly_annotated_unittests = Check.enabled; 72 | 73 | assertAnalyzerWarnings(q{ 74 | @safe unittest {} 75 | @system unittest {} 76 | pure nothrow @system @nogc unittest {} 77 | 78 | unittest {} /+ 79 | ^^^^^^^^ [warn]: %s +/ 80 | pure nothrow @nogc unittest {} /+ 81 | ^^^^^^^^ [warn]: %s +/ 82 | }c.format( 83 | ExplicitlyAnnotatedUnittestCheck.MESSAGE, 84 | ExplicitlyAnnotatedUnittestCheck.MESSAGE, 85 | ), sac); 86 | 87 | // nested 88 | assertAnalyzerWarnings(q{ 89 | struct Foo 90 | { 91 | @safe unittest {} 92 | @system unittest {} 93 | 94 | unittest {} /+ 95 | ^^^^^^^^ [warn]: %s +/ 96 | pure nothrow @nogc unittest {} /+ 97 | ^^^^^^^^ [warn]: %s +/ 98 | } 99 | }c.format( 100 | ExplicitlyAnnotatedUnittestCheck.MESSAGE, 101 | ExplicitlyAnnotatedUnittestCheck.MESSAGE, 102 | ), sac); 103 | 104 | 105 | // nested 106 | assertAutoFix(q{ 107 | unittest {} // fix:0 108 | pure nothrow @nogc unittest {} // fix:0 109 | 110 | struct Foo 111 | { 112 | unittest {} // fix:1 113 | pure nothrow @nogc unittest {} // fix:1 114 | } 115 | }c, q{ 116 | @safe unittest {} // fix:0 117 | pure nothrow @nogc @safe unittest {} // fix:0 118 | 119 | struct Foo 120 | { 121 | @system unittest {} // fix:1 122 | pure nothrow @nogc @system unittest {} // fix:1 123 | } 124 | }c, sac); 125 | 126 | stderr.writeln("Unittest for ExplicitlyAnnotatedUnittestCheck passed."); 127 | } 128 | -------------------------------------------------------------------------------- /src/dscanner/analysis/useless_assert.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.useless_assert; 7 | 8 | import dscanner.analysis.base; 9 | import dscanner.analysis.helpers; 10 | import dparse.ast; 11 | import dparse.lexer; 12 | 13 | import std.stdio; 14 | 15 | auto filterChars(string chars, S)(S str) 16 | { 17 | import std.algorithm.comparison : among; 18 | import std.algorithm.iteration : filter; 19 | import std.meta : aliasSeqOf; 20 | return str.filter!(c => !c.among(aliasSeqOf!chars)); 21 | } 22 | 23 | /** 24 | * Checks for asserts that always succeed 25 | */ 26 | final class UselessAssertCheck : BaseAnalyzer 27 | { 28 | alias visit = BaseAnalyzer.visit; 29 | 30 | mixin AnalyzerInfo!"useless_assert_check"; 31 | 32 | /// 33 | this(BaseAnalyzerArguments args) 34 | { 35 | super(args); 36 | } 37 | 38 | override void visit(const AssertExpression ae) 39 | { 40 | import std.conv : to; 41 | 42 | UnaryExpression unary = cast(UnaryExpression) ae.assertArguments.assertion; 43 | if (unary is null) 44 | return; 45 | if (unary.primaryExpression is null) 46 | return; 47 | immutable token = unary.primaryExpression.primary; 48 | immutable skipSwitch = unary.primaryExpression.arrayLiteral !is null 49 | || unary.primaryExpression.assocArrayLiteral !is null 50 | || unary.primaryExpression.functionLiteralExpression !is null; 51 | if (!skipSwitch) switch (token.type) 52 | { 53 | case tok!"doubleLiteral": 54 | if (!token.text.filterChars!"Ll".to!double) 55 | return; 56 | break; 57 | case tok!"floatLiteral": 58 | if (!token.text.filterChars!"Ff".to!float) 59 | return; 60 | break; 61 | case tok!"idoubleLiteral": 62 | case tok!"ifloatLiteral": 63 | case tok!"irealLiteral": 64 | return; // `to` doesn't support imaginary numbers 65 | case tok!"intLiteral": 66 | if (!token.text.to!int) 67 | return; 68 | break; 69 | case tok!"longLiteral": 70 | if (!token.text.filterChars!"Ll".to!long) 71 | return; 72 | break; 73 | case tok!"realLiteral": 74 | if (!token.text.to!real) 75 | return; 76 | break; 77 | case tok!"uintLiteral": 78 | if (!token.text.filterChars!"Uu".to!uint) 79 | return; 80 | break; 81 | case tok!"ulongLiteral": 82 | if (!token.text.filterChars!"UuLl".to!ulong) 83 | return; 84 | break; 85 | case tok!"characterLiteral": 86 | if (token.text == `'\0'`) 87 | return; 88 | break; 89 | case tok!"dstringLiteral": 90 | case tok!"stringLiteral": 91 | case tok!"wstringLiteral": 92 | case tok!"true": 93 | break; 94 | default: 95 | return; 96 | } 97 | addErrorMessage(unary, KEY, MESSAGE); 98 | } 99 | 100 | private: 101 | enum string KEY = "dscanner.suspicious.useless_assert"; 102 | enum string MESSAGE = "Assert condition is always true."; 103 | } 104 | 105 | unittest 106 | { 107 | import std.stdio : stderr; 108 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 109 | import std.format : format; 110 | 111 | StaticAnalysisConfig sac = disabledConfig(); 112 | sac.useless_assert_check = Check.enabled; 113 | assertAnalyzerWarnings(q{ 114 | unittest 115 | { 116 | assert(true); /+ 117 | ^^^^ [warn]: %1$s +/ 118 | assert(1); /+ 119 | ^ [warn]: %1$s +/ 120 | assert([10]); /+ 121 | ^^^^ [warn]: %1$s +/ 122 | assert(false); 123 | assert(0); 124 | assert(0.0L); 125 | } 126 | 127 | }c 128 | .format(UselessAssertCheck.MESSAGE), sac); 129 | stderr.writeln("Unittest for UselessAssertCheck passed."); 130 | } 131 | -------------------------------------------------------------------------------- /src/dscanner/analysis/redundant_storage_class.d: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, dlang-community 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.redundant_storage_class; 7 | 8 | import std.stdio; 9 | import std.string; 10 | import dparse.ast; 11 | import dparse.lexer; 12 | import dscanner.analysis.base; 13 | import dscanner.analysis.helpers; 14 | import dsymbol.scope_ : Scope; 15 | 16 | /** 17 | * Checks for redundant storage classes such immutable and __gshared, static and __gshared 18 | */ 19 | final class RedundantStorageClassCheck : BaseAnalyzer 20 | { 21 | alias visit = BaseAnalyzer.visit; 22 | enum string REDUNDANT_VARIABLE_ATTRIBUTES = "Variable declaration for `%s` has redundant attributes (%-(`%s`%|, %))."; 23 | mixin AnalyzerInfo!"redundant_storage_classes"; 24 | 25 | this(BaseAnalyzerArguments args) 26 | { 27 | super(args); 28 | } 29 | 30 | override void visit(const Declaration node) 31 | { 32 | checkAttributes(node); 33 | node.accept(this); 34 | } 35 | 36 | void checkAttributes(const Declaration node) 37 | { 38 | if (node.variableDeclaration !is null && node.attributes !is null) 39 | checkVariableDeclaration(node.variableDeclaration, node.attributes); 40 | } 41 | 42 | void checkVariableDeclaration(const VariableDeclaration vd, const Attribute[] attributes) 43 | { 44 | import std.algorithm.comparison : among; 45 | import std.algorithm.searching: all; 46 | 47 | string[] globalAttributes; 48 | foreach (attrib; attributes) 49 | { 50 | if (attrib.attribute.type.among(tok!"shared", tok!"static", tok!"__gshared", tok!"immutable")) 51 | globalAttributes ~= attrib.attribute.type.str; 52 | } 53 | if (globalAttributes.length > 1) 54 | { 55 | if (globalAttributes.length == 2 && ( 56 | globalAttributes.all!(a => a.among("shared", "static")) || 57 | globalAttributes.all!(a => a.among("static", "immutable")) 58 | )) 59 | return; 60 | auto t = vd.declarators[0].name; 61 | string message = REDUNDANT_VARIABLE_ATTRIBUTES.format(t.text, globalAttributes); 62 | addErrorMessage(t, KEY, message); 63 | } 64 | } 65 | 66 | private enum string KEY = "dscanner.unnecessary.duplicate_attribute"; 67 | } 68 | 69 | unittest 70 | { 71 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 72 | 73 | StaticAnalysisConfig sac = disabledConfig(); 74 | sac.redundant_storage_classes = Check.enabled; 75 | 76 | // https://github.com/dlang-community/D-Scanner/issues/438 77 | assertAnalyzerWarnings(q{ 78 | immutable int a; 79 | 80 | immutable shared int a; // [warn]: %s 81 | shared immutable int a; // [warn]: %s 82 | 83 | immutable __gshared int a; // [warn]: %s 84 | __gshared immutable int a; // [warn]: %s 85 | 86 | __gshared static int a; // [warn]: %s 87 | 88 | shared static int a; 89 | static shared int a; 90 | static immutable int a; 91 | immutable static int a; 92 | 93 | enum int a; 94 | extern(C++) immutable int a; 95 | immutable int function(immutable int, shared int) a; 96 | }c.format( 97 | RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["immutable", "shared"]), 98 | RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["shared", "immutable"]), 99 | RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["immutable", "__gshared"]), 100 | RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["__gshared", "immutable"]), 101 | RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["__gshared", "static"]), 102 | ), sac); 103 | 104 | stderr.writeln("Unittest for RedundantStorageClassCheck passed."); 105 | } 106 | -------------------------------------------------------------------------------- /src/dscanner/highlighter.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2012. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.highlighter; 7 | 8 | import std.stdio; 9 | import std.array; 10 | import dparse.lexer; 11 | 12 | // http://ethanschoonover.com/solarized 13 | void highlight(R)(ref R tokens, string fileName, string themeName) 14 | { 15 | immutable(Theme)* theme = getTheme(themeName); 16 | 17 | stdout.writeln(q"[ 18 | 19 | 20 | 21 | ]"); 22 | stdout.writeln("", fileName, ""); 23 | stdout.writeln(q"[ 24 | 25 | 36 |
", theme.bg, theme.fg, theme.kwrd, theme.com, theme.num, theme.str,
 37 | 			theme.op, theme.type, theme.cons);
 38 | 
 39 | 	while (!tokens.empty)
 40 | 	{
 41 | 		auto t = tokens.front;
 42 | 		tokens.popFront();
 43 | 		if (isBasicType(t.type))
 44 | 			writeSpan("type", str(t.type));
 45 | 		else if (isKeyword(t.type))
 46 | 			writeSpan("kwrd", str(t.type));
 47 | 		else if (t.type == tok!"comment")
 48 | 			writeSpan("com", t.text);
 49 | 		else if (isStringLiteral(t.type) || t.type == tok!"characterLiteral")
 50 | 			writeSpan("str", t.text);
 51 | 		else if (isNumberLiteral(t.type))
 52 | 			writeSpan("num", t.text);
 53 | 		else if (isOperator(t.type))
 54 | 			writeSpan("op", str(t.type));
 55 | 		else if (t.type == tok!"specialTokenSequence" || t.type == tok!"scriptLine")
 56 | 			writeSpan("cons", t.text.replace("<", "<"));
 57 | 		else
 58 | 		{
 59 | 			version (Windows)
 60 | 			{
 61 | 				// Stupid Windows automatically does a LF → CRLF, so
 62 | 				// CRLF → CRCRLF, which is obviously wrong.
 63 | 				// Strip out the CR characters here to avoid this.
 64 | 				stdout.write(t.text.replace("<", "<").replace("\r", ""));
 65 | 			}
 66 | 			else
 67 | 				stdout.write(t.text.replace("<", "<"));
 68 | 		}
 69 | 
 70 | 	}
 71 | 	stdout.writeln("
\n"); 72 | } 73 | 74 | void writeSpan(string cssClass, string value) 75 | { 76 | version (Windows) 77 | stdout.write(``, value.replace("&", 78 | "&").replace("<", "<").replace("\r", ""), ``); 79 | else 80 | stdout.write(``, value.replace("&", 81 | "&").replace("<", "<"), ``); 82 | } 83 | 84 | struct Theme 85 | { 86 | string bg; 87 | string fg; 88 | string kwrd; 89 | string com; 90 | string num; 91 | string str; 92 | string op; 93 | string type; 94 | string cons; 95 | } 96 | 97 | immutable(Theme)* getTheme(string themeName) 98 | { 99 | immutable Theme[string] themes = [ 100 | "solarized": Theme("#fdf6e3", "#002b36", "#b58900", "#93a1a1", "#dc322f", "#2aa198", "#586e75", 101 | "#268bd2", "#859900"), 102 | "solarized-dark": Theme("#002b36", "#fdf6e3", "#b58900", "#586e75", "#dc322f", "#2aa198", 103 | "#93a1a1", "#268bd2", "#859900"), 104 | "gruvbox": Theme("#fbf1c7", "#282828", "#b57614", "#a89984", "#9d0006", "#427b58", 105 | "#504945", "#076678", "#79740e"), 106 | "gruvbox-dark": Theme("#282828", "#fbf1c7", "#d79921", "#7c6f64", 107 | "#cc241d", "#689d6a", "#a89984", "#458588", "#98971a") 108 | ]; 109 | 110 | immutable(Theme)* theme = themeName in themes; 111 | // Default theme 112 | if (theme is null) 113 | theme = &themes["solarized"]; 114 | 115 | return theme; 116 | } 117 | -------------------------------------------------------------------------------- /src/dscanner/analysis/allman.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.allman; 6 | 7 | import dparse.lexer; 8 | import dparse.ast; 9 | import dscanner.analysis.base; 10 | import dsymbol.scope_ : Scope; 11 | 12 | import std.algorithm; 13 | import std.range; 14 | 15 | /** 16 | Checks for the allman style (braces should be on their own line) 17 | ------------ 18 | if (param < 0) { 19 | } 20 | ------------ 21 | should be 22 | ------------ 23 | if (param < 0) 24 | { 25 | } 26 | ------------ 27 | */ 28 | final class AllManCheck : BaseAnalyzer 29 | { 30 | mixin AnalyzerInfo!"allman_braces_check"; 31 | 32 | /// 33 | this(BaseAnalyzerArguments args) 34 | { 35 | super(args); 36 | foreach (i; 1 .. tokens.length - 1) 37 | { 38 | const curLine = tokens[i].line; 39 | const prevTokenLine = tokens[i-1].line; 40 | if (tokens[i].type == tok!"{" && curLine == prevTokenLine) 41 | { 42 | // ignore struct initialization 43 | if (tokens[i-1].type == tok!"=") 44 | continue; 45 | // ignore duplicate braces 46 | if (tokens[i-1].type == tok!"{" && tokens[i - 2].line != curLine) 47 | continue; 48 | // ignore inline { } braces 49 | if (curLine != tokens[i + 1].line) 50 | addErrorMessage(tokens[i], KEY, MESSAGE); 51 | } 52 | if (tokens[i].type == tok!"}" && curLine == prevTokenLine) 53 | { 54 | // ignore duplicate braces 55 | if (tokens[i-1].type == tok!"}" && tokens[i - 2].line != curLine) 56 | continue; 57 | // ignore inline { } braces 58 | if (!tokens[0 .. i].retro.until!(t => t.line != curLine).canFind!(t => t.type == tok!"{")) 59 | addErrorMessage(tokens[i], KEY, MESSAGE); 60 | } 61 | } 62 | } 63 | 64 | enum string KEY = "dscanner.style.allman"; 65 | enum string MESSAGE = "Braces should be on their own line"; 66 | } 67 | 68 | unittest 69 | { 70 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 71 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 72 | import std.format : format; 73 | import std.stdio : stderr; 74 | 75 | StaticAnalysisConfig sac = disabledConfig(); 76 | sac.allman_braces_check = Check.enabled; 77 | 78 | // check common allman style violation 79 | assertAnalyzerWarnings(q{ 80 | void testAllman() 81 | { 82 | while (true) { /+ 83 | ^ [warn]: %s +/ 84 | auto f = 1; 85 | } 86 | 87 | do { // [warn]: %s 88 | auto f = 1; 89 | } while (true); 90 | 91 | // inline braces are OK 92 | while (true) { auto f = 1; } 93 | 94 | if (true) { // [warn]: %s 95 | auto f = 1; 96 | } 97 | if (true) 98 | { 99 | auto f = 1; } // [warn]: %s 100 | if (true) { auto f = 1; } 101 | foreach (r; [1]) { // [warn]: %s 102 | } 103 | foreach (r; [1]) { } 104 | foreach_reverse (r; [1]) { // [warn]: %s 105 | } 106 | foreach_reverse (r; [1]) { } 107 | for (int i = 0; i < 10; i++) { // [warn]: %s 108 | } 109 | for (int i = 0; i < 10; i++) { } 110 | 111 | // nested check 112 | while (true) { // [warn]: %s 113 | while (true) { // [warn]: %s 114 | auto f = 1; 115 | } 116 | } 117 | } 118 | }c.format( 119 | AllManCheck.MESSAGE, 120 | AllManCheck.MESSAGE, 121 | AllManCheck.MESSAGE, 122 | AllManCheck.MESSAGE, 123 | AllManCheck.MESSAGE, 124 | AllManCheck.MESSAGE, 125 | AllManCheck.MESSAGE, 126 | AllManCheck.MESSAGE, 127 | AllManCheck.MESSAGE, 128 | ), sac); 129 | 130 | // check struct initialization 131 | assertAnalyzerWarnings(q{ 132 | unittest 133 | { 134 | struct Foo { int a; } 135 | Foo foo = { 136 | a: 1; 137 | }; 138 | } 139 | }, sac); 140 | 141 | // allow duplicate braces 142 | assertAnalyzerWarnings(q{ 143 | unittest 144 | {{ 145 | }} 146 | }, sac); 147 | 148 | 149 | stderr.writeln("Unittest for Allman passed."); 150 | } 151 | -------------------------------------------------------------------------------- /src/dscanner/imports.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2012. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.imports; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dparse.parser; 11 | import dparse.rollback_allocator; 12 | import std.stdio; 13 | import std.container.rbtree; 14 | import std.functional : toDelegate; 15 | import dscanner.utils; 16 | 17 | /** 18 | * AST visitor that collects modules imported to an R-B tree. 19 | */ 20 | class ImportPrinter : ASTVisitor 21 | { 22 | this() 23 | { 24 | imports = new RedBlackTree!string; 25 | } 26 | 27 | override void visit(const SingleImport singleImport) 28 | { 29 | ignore = false; 30 | singleImport.accept(this); 31 | ignore = true; 32 | } 33 | 34 | override void visit(const IdentifierChain identifierChain) 35 | { 36 | if (ignore) 37 | return; 38 | bool first = true; 39 | string s; 40 | foreach (ident; identifierChain.identifiers) 41 | { 42 | if (!first) 43 | s ~= "."; 44 | s ~= ident.text; 45 | first = false; 46 | } 47 | imports.insert(s); 48 | } 49 | 50 | alias visit = ASTVisitor.visit; 51 | 52 | /// Collected imports 53 | RedBlackTree!string imports; 54 | 55 | private: 56 | bool ignore = true; 57 | } 58 | 59 | private void visitFile(bool usingStdin, string fileName, RedBlackTree!string importedModules, StringCache* cache) 60 | { 61 | RollbackAllocator rba; 62 | LexerConfig config; 63 | config.fileName = fileName; 64 | config.stringBehavior = StringBehavior.source; 65 | auto visitor = new ImportPrinter; 66 | auto tokens = getTokensForParser(usingStdin ? readStdin() : readFile(fileName), config, cache); 67 | auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing)); 68 | visitor.visit(mod); 69 | importedModules.insert(visitor.imports[]); 70 | } 71 | 72 | private void doNothing(string, size_t, size_t, string, bool) 73 | { 74 | } 75 | 76 | void printImports(bool usingStdin, string[] args, string[] importPaths, StringCache* cache, bool recursive) 77 | { 78 | string[] fileNames = usingStdin ? ["stdin"] : expandArgs(args); 79 | import std.path : buildPath, dirSeparator; 80 | import std.file : isFile, exists; 81 | import std.array : replace, empty; 82 | import std.range : chain, only; 83 | 84 | auto resolvedModules = new RedBlackTree!(string); 85 | auto resolvedLocations = new RedBlackTree!(string); 86 | auto importedFiles = new RedBlackTree!(string); 87 | foreach (name; fileNames) 88 | visitFile(usingStdin, name, importedFiles, cache); 89 | if (importPaths.empty) 90 | { 91 | foreach (item; importedFiles[]) 92 | writeln(item); 93 | return; 94 | } 95 | while (!importedFiles.empty) 96 | { 97 | auto newlyDiscovered = new RedBlackTree!(string); 98 | itemLoop: foreach (item; importedFiles[]) 99 | { 100 | foreach (path; importPaths) 101 | { 102 | auto d = buildPath(path, item.replace(".", dirSeparator) ~ ".d"); 103 | auto di = buildPath(path, item.replace(".", dirSeparator) ~ ".di"); 104 | auto p = buildPath(path, item.replace(".", dirSeparator), "package.d"); 105 | auto pi = buildPath(path, item.replace(".", dirSeparator), "package.di"); 106 | foreach (alt; [d, di, p, pi]) 107 | { 108 | if (exists(alt) && isFile(alt)) 109 | { 110 | resolvedModules.insert(item); 111 | resolvedLocations.insert(alt); 112 | if (recursive) 113 | visitFile(false, alt, newlyDiscovered, cache); 114 | continue itemLoop; 115 | } 116 | } 117 | } 118 | writeln("Could not resolve location of ", item); 119 | } 120 | foreach (item; importedFiles[]) 121 | newlyDiscovered.removeKey(item); 122 | foreach (resolved; resolvedModules[]) 123 | newlyDiscovered.removeKey(resolved); 124 | importedFiles = newlyDiscovered; 125 | } 126 | foreach (resolved; resolvedLocations[]) 127 | writeln(resolved); 128 | } 129 | -------------------------------------------------------------------------------- /src/dscanner/analysis/constructors.d: -------------------------------------------------------------------------------- 1 | module dscanner.analysis.constructors; 2 | 3 | import dparse.ast; 4 | import dparse.lexer; 5 | import std.stdio; 6 | import std.typecons : Rebindable; 7 | import dscanner.analysis.base; 8 | import dscanner.analysis.helpers; 9 | import dsymbol.scope_ : Scope; 10 | 11 | final class ConstructorCheck : BaseAnalyzer 12 | { 13 | alias visit = BaseAnalyzer.visit; 14 | 15 | mixin AnalyzerInfo!"constructor_check"; 16 | 17 | this(BaseAnalyzerArguments args) 18 | { 19 | super(args); 20 | } 21 | 22 | override void visit(const ClassDeclaration classDeclaration) 23 | { 24 | const oldHasDefault = hasDefaultArgConstructor; 25 | const oldHasNoArg = hasNoArgConstructor; 26 | hasNoArgConstructor = null; 27 | hasDefaultArgConstructor = null; 28 | immutable State prev = state; 29 | state = State.inClass; 30 | classDeclaration.accept(this); 31 | if (hasNoArgConstructor && hasDefaultArgConstructor) 32 | { 33 | addErrorMessage( 34 | Message.Diagnostic.from(fileName, classDeclaration.name, 35 | "This class has a zero-argument constructor as well as a" 36 | ~ " constructor with one default argument. This can be confusing."), 37 | [ 38 | Message.Diagnostic.from(fileName, hasNoArgConstructor, "zero-argument constructor defined here"), 39 | Message.Diagnostic.from(fileName, hasDefaultArgConstructor, "default argument constructor defined here") 40 | ], 41 | "dscanner.confusing.constructor_args" 42 | ); 43 | } 44 | hasDefaultArgConstructor = oldHasDefault; 45 | hasNoArgConstructor = oldHasNoArg; 46 | state = prev; 47 | } 48 | 49 | override void visit(const StructDeclaration structDeclaration) 50 | { 51 | immutable State prev = state; 52 | state = State.inStruct; 53 | structDeclaration.accept(this); 54 | state = prev; 55 | } 56 | 57 | override void visit(const Constructor constructor) 58 | { 59 | final switch (state) 60 | { 61 | case State.inStruct: 62 | if (constructor.parameters.parameters.length == 1 63 | && constructor.parameters.parameters[0].default_ !is null) 64 | { 65 | const(Token)[] tokens = constructor.parameters.parameters[0].default_.tokens; 66 | assert(tokens.length); 67 | // we extend the token range to the `=` sign, since it's continuous 68 | tokens = (tokens.ptr - 1)[0 .. tokens.length + 1]; 69 | addErrorMessage(tokens, 70 | "dscanner.confusing.struct_constructor_default_args", 71 | "This struct constructor can never be called with its " 72 | ~ "default argument."); 73 | } 74 | break; 75 | case State.inClass: 76 | if (constructor.parameters.parameters.length == 1 77 | && constructor.parameters.parameters[0].default_ !is null) 78 | { 79 | hasDefaultArgConstructor = constructor; 80 | } 81 | else if (constructor.parameters.parameters.length == 0) 82 | hasNoArgConstructor = constructor; 83 | break; 84 | case State.ignoring: 85 | break; 86 | } 87 | } 88 | 89 | private: 90 | 91 | enum State : ubyte 92 | { 93 | ignoring, 94 | inClass, 95 | inStruct 96 | } 97 | 98 | State state; 99 | 100 | Rebindable!(const Constructor) hasNoArgConstructor; 101 | Rebindable!(const Constructor) hasDefaultArgConstructor; 102 | } 103 | 104 | unittest 105 | { 106 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 107 | 108 | StaticAnalysisConfig sac = disabledConfig(); 109 | sac.constructor_check = Check.enabled; 110 | // TODO: test supplemental diagnostics 111 | assertAnalyzerWarnings(q{ 112 | class Cat /+ 113 | ^^^ [warn]: This class has a zero-argument constructor as well as a constructor with one default argument. This can be confusing. +/ 114 | { 115 | this() {} 116 | this(string name = "kittie") {} 117 | } 118 | 119 | struct Dog 120 | { 121 | this() {} 122 | this(string name = "doggie") {} /+ 123 | ^^^^^^^^^^ [warn]: This struct constructor can never be called with its default argument. +/ 124 | } 125 | }c, sac); 126 | 127 | stderr.writeln("Unittest for ConstructorCheck passed."); 128 | } 129 | -------------------------------------------------------------------------------- /src/dscanner/analysis/if_statements.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | module dscanner.analysis.if_statements; 6 | 7 | import dparse.ast; 8 | import dparse.lexer; 9 | import dparse.formatter; 10 | import dscanner.analysis.base; 11 | import dsymbol.scope_ : Scope; 12 | import std.typecons : Rebindable, rebindable; 13 | 14 | final class IfStatementCheck : BaseAnalyzer 15 | { 16 | alias visit = BaseAnalyzer.visit; 17 | mixin AnalyzerInfo!"redundant_if_check"; 18 | 19 | this(BaseAnalyzerArguments args) 20 | { 21 | super(args); 22 | } 23 | 24 | override void visit(const IfStatement ifStatement) 25 | { 26 | import std.string : format; 27 | import std.algorithm : sort, countUntil; 28 | import std.array : appender; 29 | 30 | ++depth; 31 | 32 | if (ifStatement.condition.expression.items.length == 1 33 | && (cast(AndAndExpression) ifStatement.condition.expression.items[0]) is null) 34 | { 35 | redundancyCheck(ifStatement.condition.expression, 36 | ifStatement.condition.expression.line, ifStatement.condition.expression.column); 37 | } 38 | inIfExpresson = true; 39 | ifStatement.condition.expression.accept(this); 40 | inIfExpresson = false; 41 | ifStatement.thenStatement.accept(this); 42 | if (expressions.length) 43 | expressions = expressions[0 .. expressions.countUntil!(a => a.depth + 1 >= depth)]; 44 | if (ifStatement.elseStatement) 45 | ifStatement.elseStatement.accept(this); 46 | --depth; 47 | } 48 | 49 | override void visit(const AndAndExpression andAndExpression) 50 | { 51 | if (inIfExpresson) 52 | { 53 | redundancyCheck(andAndExpression, andAndExpression.line, andAndExpression.column); 54 | redundancyCheck(andAndExpression.left, andAndExpression.line, andAndExpression.column); 55 | redundancyCheck(andAndExpression.right, andAndExpression.line, 56 | andAndExpression.column); 57 | } 58 | andAndExpression.accept(this); 59 | } 60 | 61 | override void visit(const OrOrExpression orOrExpression) 62 | { 63 | // intentionally does nothing 64 | } 65 | 66 | private: 67 | invariant 68 | { 69 | assert(depth >= 0); 70 | } 71 | 72 | void redundancyCheck(const ExpressionNode expression, size_t line, size_t column) 73 | { 74 | import std.string : format; 75 | import std.array : appender; 76 | import std.algorithm : sort; 77 | 78 | if (expression is null) 79 | return; 80 | auto app = appender!string(); 81 | dparse.formatter.format(app, expression); 82 | immutable size_t prevLocation = alreadyChecked(app.data, line, column); 83 | if (prevLocation != size_t.max) 84 | { 85 | addErrorMessage(expressions[prevLocation].astNode, KEY, "Expression %s is true: already checked on line %d.".format( 86 | expressions[prevLocation].formatted, expressions[prevLocation].line)); 87 | } 88 | else 89 | { 90 | expressions ~= ExpressionInfo(app.data, line, column, depth, (cast(const BaseNode) expression).rebindable); 91 | sort(expressions); 92 | } 93 | } 94 | 95 | size_t alreadyChecked(string expressionText, size_t line, size_t column) 96 | { 97 | foreach (i, ref info; expressions) 98 | { 99 | if (info.line == line && info.column == column) 100 | continue; 101 | if (info.formatted == expressionText) 102 | return i; 103 | } 104 | return size_t.max; 105 | } 106 | 107 | bool inIfExpresson; 108 | int depth; 109 | ExpressionInfo[] expressions; 110 | enum string KEY = "dscanner.if_statement"; 111 | } 112 | 113 | private struct ExpressionInfo 114 | { 115 | int opCmp(ref const ExpressionInfo other) const nothrow 116 | { 117 | if (line < other.line || (line == other.line && column < other.column)) 118 | return 1; 119 | if (line == other.line && column == other.column) 120 | return 0; 121 | return -1; 122 | } 123 | 124 | string formatted; 125 | size_t line; 126 | size_t column; 127 | int depth; 128 | Rebindable!(const BaseNode) astNode; 129 | } 130 | -------------------------------------------------------------------------------- /src/dscanner/analysis/lambda_return_check.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2016. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.lambda_return_check; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import dscanner.utils : safeAccess; 12 | 13 | final class LambdaReturnCheck : BaseAnalyzer 14 | { 15 | alias visit = BaseAnalyzer.visit; 16 | 17 | mixin AnalyzerInfo!"lambda_return_check"; 18 | 19 | this(BaseAnalyzerArguments args) 20 | { 21 | super(args); 22 | } 23 | 24 | override void visit(const FunctionLiteralExpression fLit) 25 | { 26 | import std.algorithm : find; 27 | 28 | auto fe = safeAccess(fLit).assignExpression.as!UnaryExpression 29 | .primaryExpression.functionLiteralExpression.unwrap; 30 | 31 | if (fe is null || fe.parameters !is null || fe.identifier != tok!"" || 32 | fe.specifiedFunctionBody is null || fe.specifiedFunctionBody.blockStatement is null) 33 | { 34 | return; 35 | } 36 | auto start = &fLit.tokens[0]; 37 | auto endIncl = &fe.specifiedFunctionBody.tokens[0]; 38 | assert(endIncl >= start); 39 | auto tokens = start[0 .. endIncl - start + 1]; 40 | auto arrow = tokens.find!(a => a.type == tok!"=>"); 41 | 42 | AutoFix[] autofixes; 43 | if (arrow.length) 44 | { 45 | if (fLit.tokens[0] == tok!"(") 46 | autofixes ~= AutoFix.replacement(arrow[0], "", "Remove arrow (use function body)"); 47 | else 48 | autofixes ~= AutoFix.insertionBefore(fLit.tokens[0], "(", "Remove arrow (use function body)") 49 | .concat(AutoFix.insertionAfter(fLit.tokens[0], ")")) 50 | .concat(AutoFix.replacement(arrow[0], "")); 51 | } 52 | autofixes ~= AutoFix.insertionBefore(*endIncl, "() ", "Add parenthesis (return delegate)"); 53 | addErrorMessage(tokens, KEY, "This lambda returns a lambda. Add parenthesis to clarify.", 54 | autofixes); 55 | } 56 | 57 | private: 58 | enum KEY = "dscanner.confusing.lambda_returns_lambda"; 59 | } 60 | 61 | version(Windows) {/*because of newline in code*/} else 62 | unittest 63 | { 64 | import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; 65 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 66 | import std.stdio : stderr; 67 | 68 | StaticAnalysisConfig sac = disabledConfig(); 69 | sac.lambda_return_check = Check.enabled; 70 | 71 | assertAnalyzerWarnings(q{ 72 | void main() 73 | { 74 | int[] b; 75 | auto a = b.map!(a => { return a * a + 2; }).array(); /+ 76 | ^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ 77 | pragma(msg, typeof(a => { return a; })); /+ 78 | ^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ 79 | pragma(msg, typeof((a) => { return a; })); /+ 80 | ^^^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ 81 | pragma(msg, typeof({ return a; })); 82 | pragma(msg, typeof(a => () { return a; })); 83 | } 84 | }c, sac); 85 | 86 | 87 | assertAutoFix(q{ 88 | void main() 89 | { 90 | int[] b; 91 | auto a = b.map!(a => { return a * a + 2; }).array(); // fix:0 92 | auto a = b.map!(a => { return a * a + 2; }).array(); // fix:1 93 | pragma(msg, typeof(a => { return a; })); // fix:0 94 | pragma(msg, typeof(a => { return a; })); // fix:1 95 | pragma(msg, typeof((a) => { return a; })); // fix:0 96 | pragma(msg, typeof((a) => { return a; })); // fix:1 97 | } 98 | }c, q{ 99 | void main() 100 | { 101 | int[] b; 102 | auto a = b.map!((a) { return a * a + 2; }).array(); // fix:0 103 | auto a = b.map!(a => () { return a * a + 2; }).array(); // fix:1 104 | pragma(msg, typeof((a) { return a; })); // fix:0 105 | pragma(msg, typeof(a => () { return a; })); // fix:1 106 | pragma(msg, typeof((a) { return a; })); // fix:0 107 | pragma(msg, typeof((a) => () { return a; })); // fix:1 108 | } 109 | }c, sac); 110 | 111 | stderr.writeln("Unittest for LambdaReturnCheck passed."); 112 | } 113 | -------------------------------------------------------------------------------- /src/dscanner/analysis/ifelsesame.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.ifelsesame; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_ : Scope; 14 | 15 | /** 16 | * Checks for duplicated code in conditional and logical expressions. 17 | * $(UL 18 | * $(LI If statements whose "then" block is the same as the "else" block) 19 | * $(LI || and && expressions where the left and right are the same) 20 | * $(LI == expressions where the left and right are the same) 21 | * ) 22 | */ 23 | final class IfElseSameCheck : BaseAnalyzer 24 | { 25 | alias visit = BaseAnalyzer.visit; 26 | 27 | mixin AnalyzerInfo!"if_else_same_check"; 28 | 29 | this(BaseAnalyzerArguments args) 30 | { 31 | super(args); 32 | } 33 | 34 | override void visit(const IfStatement ifStatement) 35 | { 36 | if (ifStatement.thenStatement && (ifStatement.thenStatement == ifStatement.elseStatement)) 37 | { 38 | const(Token)[] tokens = ifStatement.elseStatement.tokens; 39 | // extend 1 past, so we include the `else` token 40 | tokens = (tokens.ptr - 1)[0 .. tokens.length + 1]; 41 | addErrorMessage(tokens, 42 | IF_ELSE_SAME_KEY, "'Else' branch is identical to 'Then' branch."); 43 | } 44 | ifStatement.accept(this); 45 | } 46 | 47 | override void visit(const AssignExpression assignExpression) 48 | { 49 | auto e = cast(const AssignExpression) assignExpression.expression; 50 | if (e !is null && assignExpression.operator == tok!"=" 51 | && e.ternaryExpression == assignExpression.ternaryExpression) 52 | { 53 | addErrorMessage(assignExpression, SELF_ASSIGNMENT_KEY, 54 | "Left side of assignment operatior is identical to the right side."); 55 | } 56 | assignExpression.accept(this); 57 | } 58 | 59 | override void visit(const AndAndExpression andAndExpression) 60 | { 61 | if (andAndExpression.left !is null && andAndExpression.right !is null 62 | && andAndExpression.left == andAndExpression.right) 63 | { 64 | addErrorMessage(andAndExpression.right, 65 | LOGIC_OPERATOR_OPERANDS_KEY, 66 | "Left side of logical and is identical to right side."); 67 | } 68 | andAndExpression.accept(this); 69 | } 70 | 71 | override void visit(const OrOrExpression orOrExpression) 72 | { 73 | if (orOrExpression.left !is null && orOrExpression.right !is null 74 | && orOrExpression.left == orOrExpression.right) 75 | { 76 | addErrorMessage(orOrExpression.right, 77 | LOGIC_OPERATOR_OPERANDS_KEY, 78 | "Left side of logical or is identical to right side."); 79 | } 80 | orOrExpression.accept(this); 81 | } 82 | 83 | private: 84 | 85 | enum string IF_ELSE_SAME_KEY = "dscanner.bugs.if_else_same"; 86 | enum string SELF_ASSIGNMENT_KEY = "dscanner.bugs.self_assignment"; 87 | enum string LOGIC_OPERATOR_OPERANDS_KEY = "dscanner.bugs.logic_operator_operands"; 88 | } 89 | 90 | unittest 91 | { 92 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 93 | 94 | StaticAnalysisConfig sac = disabledConfig(); 95 | sac.if_else_same_check = Check.enabled; 96 | assertAnalyzerWarnings(q{ 97 | void testSizeT() 98 | { 99 | string person = "unknown"; 100 | if (person == "unknown") 101 | person = "bobrick"; /* same */ 102 | else 103 | person = "bobrick"; /* same */ /+ 104 | ^^^^^^^^^^^^^^^^^^^^^^^ [warn]: 'Else' branch is identical to 'Then' branch. +/ 105 | // note: above ^^^ line spans over multiple lines, so it starts at start of line, since we don't have any way to test this here 106 | // look at the tests using 1-wide tab width for accurate visuals. 107 | 108 | if (person == "unknown") // ok 109 | person = "ricky"; // not same 110 | else 111 | person = "bobby"; // not same 112 | } 113 | }c, sac); 114 | 115 | assertAnalyzerWarnings(q{ 116 | void foo() 117 | { 118 | if (auto stuff = call()) 119 | } 120 | }c, sac); 121 | 122 | stderr.writeln("Unittest for IfElseSameCheck passed."); 123 | } 124 | -------------------------------------------------------------------------------- /src/dscanner/analysis/trust_too_much.d: -------------------------------------------------------------------------------- 1 | // Copyright The dlang community - 2018 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.trust_too_much; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dsymbol.scope_; 13 | 14 | /** 15 | * Checks that `@trusted` is only applied to a a single function 16 | */ 17 | final class TrustTooMuchCheck : BaseAnalyzer 18 | { 19 | private: 20 | 21 | static immutable MESSAGE = "Trusting a whole scope is a bad idea, " ~ 22 | "`@trusted` should only be attached to the functions individually"; 23 | static immutable string KEY = "dscanner.trust_too_much"; 24 | 25 | bool checkAtAttribute = true; 26 | 27 | public: 28 | 29 | alias visit = BaseAnalyzer.visit; 30 | 31 | mixin AnalyzerInfo!"trust_too_much"; 32 | 33 | /// 34 | this(BaseAnalyzerArguments args) 35 | { 36 | super(args); 37 | } 38 | 39 | override void visit(const AtAttribute d) 40 | { 41 | if (checkAtAttribute && d.identifier.text == "trusted") 42 | addErrorMessage(d, KEY, MESSAGE); 43 | d.accept(this); 44 | } 45 | 46 | // always applied to function body, so OK 47 | override void visit(const MemberFunctionAttribute d) 48 | { 49 | const oldCheckAtAttribute = checkAtAttribute; 50 | checkAtAttribute = false; 51 | d.accept(this); 52 | checkAtAttribute = oldCheckAtAttribute; 53 | } 54 | 55 | // handles `@trusted{}` and old style, leading, atAttribute for single funcs 56 | override void visit(const Declaration d) 57 | { 58 | const oldCheckAtAttribute = checkAtAttribute; 59 | 60 | checkAtAttribute = d.functionDeclaration is null && d.unittest_ is null && 61 | d.constructor is null && d.destructor is null && 62 | d.staticConstructor is null && d.staticDestructor is null && 63 | d.sharedStaticConstructor is null && d.sharedStaticDestructor is null; 64 | d.accept(this); 65 | checkAtAttribute = oldCheckAtAttribute; 66 | } 67 | 68 | // issue #588 69 | override void visit(const AliasDeclaration d) 70 | { 71 | const oldCheckAtAttribute = checkAtAttribute; 72 | checkAtAttribute = false; 73 | d.accept(this); 74 | checkAtAttribute = oldCheckAtAttribute; 75 | } 76 | } 77 | 78 | unittest 79 | { 80 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 81 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 82 | import std.format : format; 83 | 84 | StaticAnalysisConfig sac = disabledConfig(); 85 | sac.trust_too_much = Check.enabled; 86 | const msg = TrustTooMuchCheck.MESSAGE; 87 | 88 | //--- fail cases ---// 89 | 90 | assertAnalyzerWarnings(q{ 91 | @trusted: /+ 92 | ^^^^^^^^ [warn]: %s +/ 93 | void test(); 94 | }c.format(msg), sac); 95 | 96 | assertAnalyzerWarnings(q{ 97 | @trusted @nogc: /+ 98 | ^^^^^^^^ [warn]: %s +/ 99 | void test(); 100 | }c.format(msg), sac); 101 | 102 | assertAnalyzerWarnings(q{ 103 | @trusted { /+ 104 | ^^^^^^^^ [warn]: %s +/ 105 | void test(); 106 | void test(); 107 | } 108 | }c.format(msg), sac); 109 | 110 | assertAnalyzerWarnings(q{ 111 | @safe { 112 | @trusted @nogc { /+ 113 | ^^^^^^^^ [warn]: %s +/ 114 | void test(); 115 | void test(); 116 | }} 117 | }c.format(msg), sac); 118 | 119 | assertAnalyzerWarnings(q{ 120 | @nogc @trusted { /+ 121 | ^^^^^^^^ [warn]: %s +/ 122 | void test(); 123 | void test(); 124 | } 125 | }c.format(msg), sac); 126 | 127 | assertAnalyzerWarnings(q{ 128 | @trusted template foo(){ /+ 129 | ^^^^^^^^ [warn]: %s +/ 130 | } 131 | }c.format(msg), sac); 132 | 133 | assertAnalyzerWarnings(q{ 134 | struct foo{ 135 | @trusted: /+ 136 | ^^^^^^^^ [warn]: %s +/ 137 | } 138 | }c.format(msg), sac); 139 | //--- pass cases ---// 140 | 141 | assertAnalyzerWarnings(q{ 142 | void test() @trusted {} 143 | }c, sac); 144 | 145 | assertAnalyzerWarnings(q{ 146 | @trusted void test(); 147 | }c, sac); 148 | 149 | assertAnalyzerWarnings(q{ 150 | @nogc template foo(){ 151 | } 152 | }c , sac); 153 | 154 | assertAnalyzerWarnings(q{ 155 | alias nothrow @trusted uint F4(); 156 | }c , sac); 157 | 158 | assertAnalyzerWarnings(q{ 159 | @trusted ~this(); 160 | @trusted this(); 161 | }c , sac); 162 | 163 | stderr.writeln("Unittest for TrustTooMuchCheck passed."); 164 | } 165 | -------------------------------------------------------------------------------- /dscanner.ini: -------------------------------------------------------------------------------- 1 | ; Configure which static analysis checks are enabled 2 | [analysis.config.StaticAnalysisConfig] 3 | ; Check variable, class, struct, interface, union, and function names against t 4 | ; he Phobos style guide 5 | style_check="disabled" 6 | ; Check for array literals that cause unnecessary allocation 7 | enum_array_literal_check="enabled" 8 | ; Check for poor exception handling practices 9 | exception_check="enabled" 10 | ; Check for use of the deprecated 'delete' keyword 11 | delete_check="enabled" 12 | ; Check for use of the deprecated floating point operators 13 | float_operator_check="enabled" 14 | ; Check number literals for readability 15 | number_style_check="enabled" 16 | ; Checks that opEquals, opCmp, toHash, and toString are either const, immutable 17 | ; , or inout. 18 | object_const_check="enabled" 19 | ; Checks for .. expressions where the left side is larger than the right. 20 | backwards_range_check="enabled" 21 | ; Checks for if statements whose 'then' block is the same as the 'else' block 22 | if_else_same_check="enabled" 23 | ; Checks for some problems with constructors 24 | constructor_check="enabled" 25 | ; Checks for unused variables and function parameters 26 | unused_variable_check="enabled" 27 | ; Checks for unused labels 28 | unused_label_check="enabled" 29 | ; Checks for duplicate attributes 30 | duplicate_attribute="enabled" 31 | ; Checks that opEquals and toHash are both defined or neither are defined 32 | opequals_tohash_check="disabled" 33 | ; Checks for subtraction from .length properties 34 | length_subtraction_check="disabled" 35 | ; Checks for methods or properties whose names conflict with built-in propertie 36 | ; s 37 | builtin_property_names_check="enabled" 38 | ; Checks for confusing code in inline asm statements 39 | asm_style_check="enabled" 40 | ; Checks for confusing logical operator precedence 41 | logical_precedence_check="enabled" 42 | ; Checks for undocumented public declarations 43 | undocumented_declaration_check="disabled" 44 | ; Checks for poor placement of function attributes 45 | function_attribute_check="disabled" 46 | ; Checks for use of the comma operator 47 | comma_expression_check="enabled" 48 | ; Checks for local imports that are too broad 49 | local_import_check="enabled" 50 | ; Checks for variables that could be declared immutable 51 | could_be_immutable_check="disabled" 52 | ; Checks for redundant expressions in if statements 53 | redundant_if_check="enabled" 54 | ; Checks for redundant parenthesis 55 | redundant_parens_check="enabled" 56 | ; Checks for mismatched argument and parameter names 57 | mismatched_args_check="enabled" 58 | ; Checks for labels with the same name as variables 59 | label_var_same_name_check="enabled" 60 | ; Checks for lines longer than `max_line_length` characters 61 | long_line_check="disabled" 62 | ; Maximum line length in `long_line_check`. 63 | max_line_length="120" 64 | ; Checks for assignment to auto-ref function parameters 65 | auto_ref_assignment_check="enabled" 66 | ; Checks for incorrect infinite range definitions 67 | incorrect_infinite_range_check="enabled" 68 | ; Checks for asserts that are always true 69 | useless_assert_check="enabled" 70 | ; Check for uses of the old-style alias syntax 71 | alias_syntax_check="enabled" 72 | ; Checks for else if that should be else static if 73 | static_if_else_check="enabled" 74 | ; Check for unclear lambda syntax 75 | lambda_return_check="enabled" 76 | ; Check for auto function without return statement 77 | auto_function_check="enabled" 78 | ; Check for sortedness of imports 79 | imports_sortedness="disabled" 80 | ; Check for explicitly annotated unittests 81 | explicitly_annotated_unittests="disabled" 82 | ; Check for properly documented public functions (Returns, Params) 83 | properly_documented_public_functions="disabled" 84 | ; Check for useless usage of the final attribute 85 | final_attribute_check="enabled" 86 | ; Check for virtual calls in the class constructors 87 | vcall_in_ctor="enabled" 88 | ; Check for useless user defined initializers 89 | useless_initializer="disabled" 90 | ; Check allman brace style 91 | allman_braces_check="disabled" 92 | ; Check for redundant attributes 93 | redundant_attributes_check="enabled" 94 | ; Check for unused function return values 95 | unused_result="enabled" 96 | ; Enable cyclomatic complexity check 97 | cyclomatic_complexity="disabled" 98 | ; Maximum cyclomatic complexity after which to issue warnings 99 | max_cyclomatic_complexity="50" 100 | ; Check for function bodies on disabled functions 101 | body_on_disabled_func_check="enabled" 102 | -------------------------------------------------------------------------------- /src/dscanner/symbol_finder.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.symbol_finder; 7 | 8 | import std.stdio : File; 9 | import dparse.lexer; 10 | import dparse.parser; 11 | import dparse.ast; 12 | import dparse.rollback_allocator; 13 | import std.stdio; 14 | import std.file : isFile; 15 | import std.functional : toDelegate; 16 | 17 | void findDeclarationOf(File output, string symbolName, string[] fileNames) 18 | { 19 | findDeclarationOf((string fileName, size_t line, size_t column) 20 | { 21 | output.writefln("%s(%d:%d)", fileName, line, column); 22 | }, symbolName, fileNames); 23 | } 24 | 25 | /// Delegate that gets called every time a declaration gets found 26 | alias OutputHandler = void delegate(string fileName, size_t line, size_t column); 27 | 28 | /// Finds all declarations of a symbol in the given fileNames and calls a handler on every occurence. 29 | /// Params: 30 | /// output = Callback which gets called when a declaration is found 31 | /// symbolName = Symbol name to search for 32 | /// fileNames = An array of file names which might contain stdin to read from stdin 33 | void findDeclarationOf(scope OutputHandler output, string symbolName, string[] fileNames) 34 | { 35 | import std.array : uninitializedArray, array; 36 | import std.conv : to; 37 | 38 | LexerConfig config; 39 | StringCache cache = StringCache(StringCache.defaultBucketCount); 40 | auto visitor = new FinderVisitor(output, symbolName); 41 | foreach (fileName; fileNames) 42 | { 43 | File f = fileName == "stdin" ? std.stdio.stdin : File(fileName); 44 | assert(fileName == "stdin" || isFile(fileName)); 45 | if (f.size == 0) 46 | continue; 47 | auto bytes = uninitializedArray!(ubyte[])(to!size_t(f.size)); 48 | f.rawRead(bytes); 49 | auto tokens = getTokensForParser(bytes, config, &cache); 50 | RollbackAllocator rba; 51 | Module m = parseModule(tokens.array, fileName, &rba, toDelegate(&doNothing)); 52 | visitor.fileName = fileName; 53 | visitor.visit(m); 54 | } 55 | } 56 | 57 | private: 58 | 59 | void doNothing(string, size_t, size_t, string, bool) 60 | { 61 | } 62 | 63 | class FinderVisitor : ASTVisitor 64 | { 65 | this(OutputHandler output, string symbolName) 66 | { 67 | this.output = output; 68 | this.symbolName = symbolName; 69 | } 70 | 71 | mixin generateVisit!FunctionDeclaration; 72 | mixin generateVisit!ClassDeclaration; 73 | mixin generateVisit!InterfaceDeclaration; 74 | mixin generateVisit!StructDeclaration; 75 | mixin generateVisit!UnionDeclaration; 76 | mixin generateVisit!TemplateDeclaration; 77 | 78 | override void visit(const EnumDeclaration dec) 79 | { 80 | if (dec.name.text == symbolName) 81 | output(fileName, dec.name.line, dec.name.column); 82 | } 83 | 84 | override void visit(const AnonymousEnumMember member) 85 | { 86 | if (member.name.text == symbolName) 87 | output(fileName, member.name.line, member.name.column); 88 | } 89 | 90 | override void visit(const EnumMember member) 91 | { 92 | if (member.name.text == symbolName) 93 | output(fileName, member.name.line, member.name.column); 94 | } 95 | 96 | override void visit(const AliasDeclaration dec) 97 | { 98 | if (dec.declaratorIdentifierList !is null) 99 | { 100 | foreach (ident; dec.declaratorIdentifierList.identifiers) 101 | { 102 | if (ident.text == symbolName) 103 | output(fileName, ident.line, ident.column); 104 | } 105 | } 106 | foreach (initializer; dec.initializers) 107 | { 108 | if (initializer.name.text == symbolName) 109 | output(fileName, initializer.name.line, 110 | initializer.name.column); 111 | } 112 | } 113 | 114 | override void visit(const Declarator dec) 115 | { 116 | if (dec.name.text == symbolName) 117 | output(fileName, dec.name.line, dec.name.column); 118 | } 119 | 120 | override void visit(const AutoDeclaration ad) 121 | { 122 | foreach (part; ad.parts) 123 | { 124 | if (part.identifier.text == symbolName) 125 | output(fileName, part.identifier.line, part.identifier.column); 126 | } 127 | } 128 | 129 | override void visit(const FunctionBody) 130 | { 131 | } 132 | 133 | mixin template generateVisit(T) 134 | { 135 | override void visit(const T t) 136 | { 137 | if (t.name.text == symbolName) 138 | output(fileName, t.name.line, t.name.column); 139 | t.accept(this); 140 | } 141 | } 142 | 143 | alias visit = ASTVisitor.visit; 144 | 145 | OutputHandler output; 146 | string symbolName; 147 | string fileName; 148 | } 149 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test clean 2 | 3 | DC ?= dmd 4 | GIT ?= git 5 | DMD := $(DC) 6 | GDC := gdc 7 | LDC := ldc2 8 | 9 | LIB_SRC := \ 10 | $(shell find containers/src -name "*.d")\ 11 | $(shell find DCD/dsymbol/src -name "*.d")\ 12 | $(shell find inifiled/source/ -name "*.d")\ 13 | $(shell find libdparse/src/std/experimental/ -name "*.d")\ 14 | $(shell find libdparse/src/dparse/ -name "*.d")\ 15 | $(shell find libddoc/src -name "*.d") \ 16 | $(shell find libddoc/common/source -name "*.d") 17 | PROJECT_SRC := $(shell find src/ -name "*.d") 18 | SRC := $(LIB_SRC) $(PROJECT_SRC) 19 | 20 | OBJ_DIR := obj 21 | UT_OBJ_DIR = $(OBJ_DIR)/unittest 22 | OBJ = $(SRC:.d=.o) 23 | PROJECT_OBJ = $(PROJECT_SRC:.d=.o) 24 | LIB_OBJ = $(LIB_SRC:.d=.o) 25 | 26 | OBJ_BY_DC = $(addprefix $(OBJ_DIR)/$(DC)/, $(OBJ)) 27 | # `sort` also removes duplicates, which is what we want 28 | OBJ_BY_DC_DIR = $(sort $(dir $(OBJ_BY_DC))) 29 | UT_OBJ_BY_DC = $(addprefix $(UT_OBJ_DIR)/$(DC)/, $(PROJECT_OBJ)) 30 | UT_OBJ_BY_DC_DIR = $(sort $(dir $(UT_OBJ_BY_DC))) 31 | 32 | DSCANNER_BIN = bin/dscanner 33 | DSCANNER_BIN_DIR = $(dir $(DSCANNER_BIN)) 34 | UT_DSCANNER_BIN = bin/dscanner-unittest 35 | UT_DSCANNER_LIB = bin/$(DC)/dscanner-unittest-lib.a 36 | UT_DSCANNER_LIB_DIR = $(dir $(UT_DSCANNER_LIB)) 37 | 38 | INCLUDE_PATHS = \ 39 | -Isrc \ 40 | -Iinifiled/source \ 41 | -Ilibdparse/src \ 42 | -IDCD/dsymbol/src \ 43 | -Icontainers/src \ 44 | -Ilibddoc/src \ 45 | -Ilibddoc/common/source 46 | 47 | # e.g. "-version=MyCustomVersion" 48 | DMD_VERSIONS = 49 | DMD_DEBUG_VERSIONS = -version=dparse_verbose 50 | # e.g. "-d-version=MyCustomVersion" 51 | LDC_VERSIONS = 52 | LDC_DEBUG_VERSIONS = -d-version=dparse_verbose 53 | # e.g. "-fversion=MyCustomVersion" 54 | GDC_VERSIONS = 55 | GDC_DEBUG_VERSIONS = -fversion=dparse_verbose 56 | 57 | DC_FLAGS += -Jbin 58 | override DMD_FLAGS += $(DFLAGS) -w -release -O -od${OBJ_DIR} 59 | override LDC_FLAGS += $(DFLAGS) -O5 -release -oq 60 | override GDC_FLAGS += $(DFLAGS) -O3 -frelease -fall-instantiations 61 | 62 | override GDC_TEST_FLAGS += -fall-instantiations 63 | 64 | DC_TEST_FLAGS += -g -Jbin 65 | override DMD_TEST_FLAGS += -w 66 | 67 | DC_DEBUG_FLAGS := -g -Jbin 68 | 69 | ifeq ($(DC), $(filter $(DC), dmd ldmd2 gdmd)) 70 | VERSIONS := $(DMD_VERSIONS) 71 | DEBUG_VERSIONS := $(DMD_DEBUG_VERSIONS) 72 | DC_FLAGS += $(DMD_FLAGS) 73 | DC_TEST_FLAGS += $(DMD_TEST_FLAGS) -unittest 74 | WRITE_TO_TARGET_NAME = -of$@ 75 | else ifneq (,$(findstring ldc2, $(DC))) 76 | VERSIONS := $(LDC_VERSIONS) 77 | DEBUG_VERSIONS := $(LDC_DEBUG_VERSIONS) 78 | DC_FLAGS += $(LDC_FLAGS) 79 | DC_TEST_FLAGS += $(LDC_TEST_FLAGS) -unittest 80 | WRITE_TO_TARGET_NAME = -of=$@ 81 | else ifneq (,$(findstring gdc, $(DC))) 82 | VERSIONS := $(GDC_VERSIONS) 83 | DEBUG_VERSIONS := $(GDC_DEBUG_VERSIONS) 84 | DC_FLAGS += $(GDC_FLAGS) 85 | DC_TEST_FLAGS += $(GDC_TEST_FLAGS) -funittest 86 | WRITE_TO_TARGET_NAME = -o $@ 87 | endif 88 | 89 | GITHASH = bin/githash.txt 90 | 91 | 92 | $(OBJ_DIR)/$(DC)/%.o: %.d 93 | ${DC} ${DC_FLAGS} ${VERSIONS} ${INCLUDE_PATHS} -c $< ${WRITE_TO_TARGET_NAME} 94 | 95 | $(UT_OBJ_DIR)/$(DC)/%.o: %.d 96 | ${DC} ${DC_TEST_FLAGS} ${VERSIONS} ${INCLUDE_PATHS} -c $< ${WRITE_TO_TARGET_NAME} 97 | 98 | ${DSCANNER_BIN}: ${GITHASH} ${OBJ_BY_DC} | ${DSCANNER_BIN_DIR} 99 | ${DC} ${OBJ_BY_DC} ${WRITE_TO_TARGET_NAME} 100 | 101 | ${OBJ_BY_DC}: | ${OBJ_BY_DC_DIR} 102 | 103 | ${OBJ_BY_DC_DIR}: 104 | mkdir -p $@ 105 | 106 | ${UT_OBJ_BY_DC}: | ${UT_OBJ_BY_DC_DIR} 107 | 108 | ${UT_OBJ_BY_DC_DIR}: 109 | mkdir -p $@ 110 | 111 | ${DSCANNER_BIN_DIR}: 112 | mkdir -p $@ 113 | 114 | ${UT_DSCANNER_LIB_DIR}: 115 | mkdir -p $@ 116 | 117 | all: ${DSCANNER_BIN} 118 | ldc: ${DSCANNER_BIN} 119 | gdc: ${DSCANNER_BIN} 120 | 121 | githash: ${GITHASH} 122 | 123 | ${GITHASH}: 124 | mkdir -p bin && ${GIT} describe --tags --always > ${GITHASH} 125 | 126 | debug: ${GITHASH} 127 | ${DC} -w -g -Jbin -ofdsc ${VERSIONS} ${DEBUG_VERSIONS} ${INCLUDE_PATHS} ${SRC} 128 | 129 | # compile the dependencies separately, s.t. their unittests don't get executed 130 | ${UT_DSCANNER_LIB}: ${LIB_SRC} | ${UT_DSCANNER_LIB_DIR} 131 | ${DC} ${DC_DEBUG_FLAGS} -c ${VERSIONS} ${INCLUDE_PATHS} ${LIB_SRC} ${WRITE_TO_TARGET_NAME} 132 | 133 | test: ${UT_DSCANNER_BIN} 134 | 135 | ${UT_DSCANNER_BIN}: ${UT_DSCANNER_LIB} ${GITHASH} ${UT_OBJ_BY_DC} | ${DSCANNER_BIN_DIR} 136 | ${DC} ${UT_DSCANNER_LIB} ${UT_OBJ_BY_DC} ${WRITE_TO_TARGET_NAME} 137 | ./${UT_DSCANNER_BIN} 138 | 139 | lint: ${DSCANNER_BIN} 140 | ./${DSCANNER_BIN} --styleCheck src 141 | 142 | clean: 143 | rm -rf dsc 144 | rm -rf bin 145 | rm -rf ${OBJ_DIR} 146 | rm -f dscanner-report.json 147 | 148 | report: all 149 | dscanner --report src > src/dscanner-report.json 150 | sonar-runner 151 | 152 | release: 153 | ./release.sh 154 | -------------------------------------------------------------------------------- /src/dscanner/analysis/static_if_else.d: -------------------------------------------------------------------------------- 1 | // Copyright Chris Wright (dhasenan) 2016. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.static_if_else; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import dscanner.utils : safeAccess; 12 | 13 | /** 14 | * Checks for potentially mistaken static if / else if. 15 | * 16 | * It's potentially valid to write: 17 | * --- 18 | * static if (foo) { 19 | * } else if (bar) { 20 | * } 21 | * --- 22 | * 23 | * However, it's more likely that this is a mistake. 24 | */ 25 | final class StaticIfElse : BaseAnalyzer 26 | { 27 | alias visit = BaseAnalyzer.visit; 28 | 29 | mixin AnalyzerInfo!"static_if_else_check"; 30 | 31 | this(BaseAnalyzerArguments args) 32 | { 33 | super(args); 34 | } 35 | 36 | override void visit(const ConditionalStatement cc) 37 | { 38 | cc.accept(this); 39 | if (cc.falseStatement is null) 40 | { 41 | return; 42 | } 43 | const(IfStatement) ifStmt = getIfStatement(cc); 44 | if (!ifStmt) 45 | { 46 | return; 47 | } 48 | auto tokens = ifStmt.tokens[0 .. 1]; 49 | // extend one token to include `else` before this if 50 | tokens = (tokens.ptr - 1)[0 .. 2]; 51 | addErrorMessage(tokens, KEY, "Mismatched static if. Use 'else static if' here.", 52 | [ 53 | AutoFix.insertionBefore(tokens[$ - 1], "static "), 54 | AutoFix.resolveLater("Wrap '{}' block around 'if'", [tokens[0].index, ifStmt.tokens[$ - 1].index, 0]) 55 | ]); 56 | } 57 | 58 | const(IfStatement) getIfStatement(const ConditionalStatement cc) 59 | { 60 | return safeAccess(cc).falseStatement.statement.statementNoCaseNoDefault.ifStatement; 61 | } 62 | 63 | override AutoFix.CodeReplacement[] resolveAutoFix( 64 | const Module mod, 65 | scope const(Token)[] tokens, 66 | const AutoFix.ResolveContext context, 67 | const AutoFixFormatting formatting, 68 | ) 69 | { 70 | import dscanner.analysis.helpers : getLineIndentation; 71 | import std.algorithm : countUntil; 72 | 73 | auto beforeElse = tokens.countUntil!(a => a.index == context.params[0]); 74 | auto lastToken = tokens.countUntil!(a => a.index == context.params[1]); 75 | if (beforeElse == -1 || lastToken == -1) 76 | throw new Exception("got different tokens than what was used to generate this autofix"); 77 | 78 | auto indentation = getLineIndentation(tokens, tokens[beforeElse].line, formatting); 79 | 80 | string beforeIf = formatting.getWhitespaceBeforeOpeningBrace(indentation, false) 81 | ~ "{" ~ formatting.eol ~ indentation; 82 | string afterIf = formatting.eol ~ indentation ~ "}"; 83 | 84 | return AutoFix.replacement([tokens[beforeElse].index + 4, tokens[beforeElse + 1].index], beforeIf, "") 85 | .concat(AutoFix.indentLines(tokens[beforeElse + 1 .. lastToken + 1], formatting)) 86 | .concat(AutoFix.insertionAfter(tokens[lastToken], afterIf)) 87 | .expectReplacements; 88 | } 89 | 90 | enum KEY = "dscanner.suspicious.static_if_else"; 91 | } 92 | 93 | unittest 94 | { 95 | import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; 96 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 97 | import std.stdio : stderr; 98 | 99 | StaticAnalysisConfig sac = disabledConfig(); 100 | sac.static_if_else_check = Check.enabled; 101 | assertAnalyzerWarnings(q{ 102 | void foo() { 103 | static if (false) 104 | auto a = 0; 105 | else if (true) /+ 106 | ^^^^^^^ [warn]: Mismatched static if. Use 'else static if' here. +/ 107 | auto b = 1; 108 | } 109 | }c, sac); 110 | // Explicit braces, so no warning. 111 | assertAnalyzerWarnings(q{ 112 | void foo() { 113 | static if (false) 114 | auto a = 0; 115 | else { 116 | if (true) 117 | auto b = 1; 118 | } 119 | } 120 | }c, sac); 121 | 122 | assertAutoFix(q{ 123 | void foo() { 124 | static if (false) 125 | auto a = 0; 126 | else if (true) // fix:0 127 | auto b = 1; 128 | } 129 | void bar() { 130 | static if (false) 131 | auto a = 0; 132 | else if (true) // fix:1 133 | auto b = 1; 134 | } 135 | void baz() { 136 | static if (false) 137 | auto a = 0; 138 | else if (true) { // fix:1 139 | auto b = 1; 140 | } 141 | } 142 | }c, q{ 143 | void foo() { 144 | static if (false) 145 | auto a = 0; 146 | else static if (true) // fix:0 147 | auto b = 1; 148 | } 149 | void bar() { 150 | static if (false) 151 | auto a = 0; 152 | else { 153 | if (true) // fix:1 154 | auto b = 1; 155 | } 156 | } 157 | void baz() { 158 | static if (false) 159 | auto a = 0; 160 | else { 161 | if (true) { // fix:1 162 | auto b = 1; 163 | } 164 | } 165 | } 166 | }c, sac); 167 | 168 | stderr.writeln("Unittest for StaticIfElse passed."); 169 | } 170 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | platform: x64 2 | environment: 3 | matrix: 4 | - DC: dmd 5 | DVersion: stable 6 | arch: x64 7 | - DC: dmd 8 | DVersion: stable 9 | arch: x86 10 | #- DC: ldc 11 | #DVersion: beta 12 | #arch: x86 13 | #- DC: ldc 14 | #DVersion: beta 15 | #arch: x64 16 | #- DC: ldc 17 | #DVersion: stable 18 | #arch: x86 19 | - DC: ldc 20 | DVersion: stable 21 | arch: x64 22 | 23 | skip_tags: false 24 | branches: 25 | only: 26 | - master 27 | - /^v\d+\.\d+\.\d+([+-]\S*)*$/ 28 | 29 | init: 30 | - git config --global core.autocrlf true 31 | 32 | install: 33 | - ps: function ResolveLatestDMD 34 | { 35 | $version = $env:DVersion; 36 | if($version -eq "stable") { 37 | $latest = (Invoke-WebRequest "http://downloads.dlang.org/releases/LATEST").toString(); 38 | $url = "http://downloads.dlang.org/releases/2.x/$($latest)/dmd.$($latest).windows.7z"; 39 | }elseif($version -eq "beta") { 40 | $latest = (Invoke-WebRequest "http://downloads.dlang.org/pre-releases/LATEST").toString(); 41 | $latestVersion = $latest.split("-")[0].split("~")[0]; 42 | $url = "http://downloads.dlang.org/pre-releases/2.x/$($latestVersion)/dmd.$($latest).windows.7z"; 43 | }elseif($version -eq "nightly") { 44 | $url = "http://nightlies.dlang.org/dmd-master-2017-05-20/dmd.master.windows.7z" 45 | }else { 46 | $url = "http://downloads.dlang.org/releases/2.x/$($version)/dmd.$($version).windows.7z"; 47 | } 48 | $env:PATH += ";C:\dmd2\windows\bin;"; 49 | return $url; 50 | } 51 | - ps: function ResolveLatestLDC 52 | { 53 | $version = $env:DVersion; 54 | if($version -eq "stable") { 55 | $latest = (Invoke-WebRequest "https://ldc-developers.github.io/LATEST").toString().replace("`n","").replace("`r",""); 56 | $url = "https://github.com/ldc-developers/ldc/releases/download/v$($latest)/ldc2-$($latest)-windows-x64.7z"; 57 | }elseif($version -eq "beta") { 58 | $latest = (Invoke-WebRequest "https://ldc-developers.github.io/LATEST_BETA").toString().replace("`n","").replace("`r",""); 59 | $url = "https://github.com/ldc-developers/ldc/releases/download/v$($latest)/ldc2-$($latest)-windows-x64.7z"; 60 | } else { 61 | $latest = $version; 62 | $url = "https://github.com/ldc-developers/ldc/releases/download/v$($version)/ldc2-$($version)-win64-msvc.zip"; 63 | } 64 | $env:PATH += ";C:\ldc2-$($latest)-windows-x64\bin"; 65 | $env:DC = "ldc2"; 66 | return $url; 67 | } 68 | - ps: function SetUpDCompiler 69 | { 70 | $env:toolchain = "msvc"; 71 | if($env:DC -eq "dmd"){ 72 | echo "downloading ..."; 73 | $url = ResolveLatestDMD; 74 | echo $url; 75 | Invoke-WebRequest $url -OutFile "c:\dmd.7z"; 76 | echo "finished."; 77 | pushd c:\\; 78 | 7z x dmd.7z > $null; 79 | popd; 80 | } 81 | elseif($env:DC -eq "ldc"){ 82 | echo "downloading ..."; 83 | $url = ResolveLatestLDC; 84 | echo $url; 85 | Invoke-WebRequest $url -OutFile "c:\ldc.7z"; 86 | echo "finished."; 87 | pushd c:\\; 88 | 7z x ldc.7z > $null; 89 | popd; 90 | } 91 | } 92 | - ps: SetUpDCompiler 93 | 94 | build_script: 95 | - ps: if($env:arch -eq "x86"){ 96 | $env:compilersetupargs = "x86"; 97 | $env:Darch = "x86"; 98 | $env:DConf = "m32"; 99 | }elseif($env:arch -eq "x64"){ 100 | $env:compilersetupargs = "amd64"; 101 | $env:Darch = "x86_64"; 102 | $env:DConf = "m64"; 103 | } 104 | - ps: $env:compilersetup = "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall"; 105 | - '"%compilersetup%" %compilersetupargs%' 106 | 107 | test_script: 108 | - echo %PLATFORM% 109 | - echo %Darch% 110 | - echo %DC% 111 | - echo %PATH% 112 | - '%DC% --version' 113 | - dub test --arch=%Darch% --compiler=%DC% 114 | - git submodule update --init --recursive 115 | - build.bat test 116 | - build.bat 117 | 118 | deploy: 119 | release: dscanner-v$(appveyor_build_version) 120 | description: 'DScanner release' 121 | provider: GitHub 122 | auth_token: 123 | secure: FhQH4pdE0v2jKANNhX5wlm1oKBfizXyArWUskWfL/bmxaTaLjeyduTzotBTzNQ4p 124 | artifact: bin\dscanner.exe # upload D-Scanner binary 125 | draft: false 126 | prerelease: true 127 | on: 128 | branch: master # release from master branch only 129 | appveyor_repo_tag: true # deploy on tag push only 130 | -------------------------------------------------------------------------------- /src/dscanner/outliner.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2012. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.outliner; 7 | 8 | import dparse.lexer; 9 | import dparse.ast; 10 | import dparse.formatter; 11 | import std.stdio; 12 | import std.string; 13 | import std.array; 14 | import std.conv; 15 | 16 | class Outliner : ASTVisitor 17 | { 18 | alias visit = ASTVisitor.visit; 19 | 20 | this(File output) 21 | { 22 | this.output = output; 23 | } 24 | 25 | override void visit(const ClassDeclaration classDec) 26 | { 27 | printIndentation(); 28 | output.writeln("class ", classDec.name.text, " : ", classDec.name.line); 29 | indent(); 30 | classDec.accept(this); 31 | outdent(); 32 | finish(); 33 | } 34 | 35 | override void visit(const EnumDeclaration enumDec) 36 | { 37 | printIndentation(); 38 | output.writeln("enum ", enumDec.name.text, " : ", enumDec.name.line); 39 | indent(); 40 | enumDec.accept(this); 41 | outdent(); 42 | finish(); 43 | } 44 | 45 | override void visit(const AnonymousEnumMember enumMem) 46 | { 47 | printIndentation(); 48 | if (enumMem.type !is null) 49 | { 50 | auto app = appender!(char[])(); 51 | auto f = new Formatter!(typeof(app))(app); 52 | f.format(enumMem.type); 53 | output.writeln("enum ", app.data, " ", enumMem.name.text, " : ", enumMem.name.line); 54 | } 55 | else 56 | output.writeln("enum ", enumMem.name.text, " : ", enumMem.name.line); 57 | finish(); 58 | } 59 | 60 | override void visit(const EnumMember enumMem) 61 | { 62 | printIndentation(); 63 | output.writeln(enumMem.name.text, " : ", enumMem.name.line); 64 | finish(); 65 | } 66 | 67 | override void visit(const FunctionDeclaration functionDec) 68 | { 69 | printIndentation(); 70 | if (functionDec.hasAuto) 71 | output.write("auto "); 72 | if (functionDec.hasRef) 73 | output.write("ref "); 74 | auto app = appender!(char[])(); 75 | auto f = new Formatter!(typeof(app))(app); 76 | if (functionDec.returnType !is null) 77 | f.format(functionDec.returnType); 78 | app.put(" "); 79 | app.put(functionDec.name.text); 80 | f.format(functionDec.parameters); 81 | app.put(" : "); 82 | app.put(to!string(functionDec.name.line)); 83 | output.writeln(app.data); 84 | finish(); 85 | } 86 | 87 | override void visit(const InterfaceDeclaration interfaceDec) 88 | { 89 | printIndentation(); 90 | output.writeln("interface ", interfaceDec.name.text, " : ", interfaceDec.name.line); 91 | indent(); 92 | interfaceDec.accept(this); 93 | outdent(); 94 | finish(); 95 | } 96 | 97 | override void visit(const StructDeclaration structDec) 98 | { 99 | printIndentation(); 100 | output.writeln("struct ", structDec.name.text, " : ", structDec.name.line); 101 | indent(); 102 | structDec.accept(this); 103 | outdent(); 104 | finish(); 105 | } 106 | 107 | override void visit(const TemplateDeclaration templateDeclaration) 108 | { 109 | printIndentation(); 110 | output.writeln("template ", templateDeclaration.name.text, " : ", 111 | templateDeclaration.name.line); 112 | indent(); 113 | templateDeclaration.accept(this); 114 | outdent(); 115 | finish(); 116 | } 117 | 118 | //dfmt off 119 | override void visit(const StaticConstructor s) {} 120 | override void visit(const StaticDestructor s) {} 121 | override void visit(const SharedStaticConstructor s) {} 122 | override void visit(const SharedStaticDestructor s) {} 123 | override void visit(const Constructor c) {} 124 | override void visit(const Unittest u) {} 125 | // dfmt on 126 | 127 | override void visit(const UnionDeclaration unionDeclaration) 128 | { 129 | printIndentation(); 130 | output.writeln("union ", unionDeclaration.name.text, " : ", unionDeclaration.name.line); 131 | indent(); 132 | unionDeclaration.accept(this); 133 | outdent(); 134 | finish(); 135 | } 136 | 137 | override void visit(const VariableDeclaration variableDeclaration) 138 | { 139 | foreach (const Declarator d; variableDeclaration.declarators) 140 | { 141 | printIndentation(); 142 | auto app = appender!(char[])(); 143 | if (variableDeclaration.type !is null) 144 | { 145 | auto f = new Formatter!(typeof(app))(app); 146 | f.format(variableDeclaration.type); 147 | } 148 | output.writeln(app.data, " ", d.name.text, " : ", d.name.line); 149 | } 150 | finish(); 151 | } 152 | 153 | private: 154 | 155 | void finish() 156 | { 157 | if (indentLevel == 0) 158 | output.writeln(); 159 | } 160 | 161 | void printIndentation() 162 | { 163 | foreach (i; 0 .. indentLevel) 164 | output.write(" "); 165 | } 166 | 167 | void indent() 168 | { 169 | indentLevel++; 170 | } 171 | 172 | void outdent() 173 | { 174 | indentLevel--; 175 | } 176 | 177 | int indentLevel; 178 | 179 | File output; 180 | } 181 | -------------------------------------------------------------------------------- /src/dscanner/analysis/assert_without_msg.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.assert_without_msg; 6 | 7 | import dscanner.analysis.base; 8 | import dscanner.utils : safeAccess; 9 | import dsymbol.scope_ : Scope; 10 | import dparse.lexer; 11 | import dparse.ast; 12 | 13 | import std.stdio; 14 | import std.algorithm; 15 | 16 | /** 17 | * Check that all asserts have an explanatory message. 18 | */ 19 | final class AssertWithoutMessageCheck : BaseAnalyzer 20 | { 21 | enum string KEY = "dscanner.style.assert_without_msg"; 22 | enum string MESSAGE = "An assert should have an explanatory message"; 23 | mixin AnalyzerInfo!"assert_without_msg"; 24 | 25 | /// 26 | this(BaseAnalyzerArguments args) 27 | { 28 | super(args); 29 | } 30 | 31 | override void visit(const AssertExpression expr) 32 | { 33 | static if (__traits(hasMember, expr.assertArguments, "messageParts")) 34 | { 35 | // libdparse 0.22.0+ 36 | bool hasMessage = expr.assertArguments 37 | && expr.assertArguments.messageParts.length > 0; 38 | } 39 | else 40 | bool hasMessage = expr.assertArguments 41 | && expr.assertArguments.message !is null; 42 | 43 | if (!hasMessage) 44 | addErrorMessage(expr, KEY, MESSAGE); 45 | } 46 | 47 | override void visit(const FunctionCallExpression expr) 48 | { 49 | if (!isStdExceptionImported) 50 | return; 51 | 52 | if (const IdentifierOrTemplateInstance iot = safeAccess(expr) 53 | .unaryExpression.primaryExpression.identifierOrTemplateInstance) 54 | { 55 | auto ident = iot.identifier; 56 | if (ident.text == "enforce" && expr.arguments !is null && expr.arguments.namedArgumentList !is null && 57 | expr.arguments.namedArgumentList.items.length < 2) 58 | addErrorMessage(expr, KEY, MESSAGE); 59 | } 60 | } 61 | 62 | override void visit(const SingleImport sImport) 63 | { 64 | static immutable stdException = ["std", "exception"]; 65 | if (sImport.identifierChain.identifiers.map!(a => a.text).equal(stdException)) 66 | isStdExceptionImported = true; 67 | } 68 | 69 | // revert the stack after new scopes 70 | override void visit(const Declaration decl) 71 | { 72 | // be careful - ImportDeclarations don't introduce a new scope 73 | if (decl.importDeclaration is null) 74 | { 75 | bool tmp = isStdExceptionImported; 76 | scope(exit) isStdExceptionImported = tmp; 77 | decl.accept(this); 78 | } 79 | else 80 | decl.accept(this); 81 | } 82 | 83 | mixin ScopedVisit!IfStatement; 84 | mixin ScopedVisit!BlockStatement; 85 | 86 | alias visit = BaseAnalyzer.visit; 87 | 88 | private: 89 | bool isStdExceptionImported; 90 | 91 | template ScopedVisit(NodeType) 92 | { 93 | override void visit(const NodeType n) 94 | { 95 | bool tmp = isStdExceptionImported; 96 | scope(exit) isStdExceptionImported = tmp; 97 | n.accept(this); 98 | } 99 | } 100 | } 101 | 102 | unittest 103 | { 104 | import std.stdio : stderr; 105 | import std.format : format; 106 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 107 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 108 | 109 | StaticAnalysisConfig sac = disabledConfig(); 110 | sac.assert_without_msg = Check.enabled; 111 | 112 | assertAnalyzerWarnings(q{ 113 | unittest { 114 | assert(0, "foo bar"); 115 | assert(0); /+ 116 | ^^^^^^^^^ [warn]: %s +/ 117 | } 118 | }c.format( 119 | AssertWithoutMessageCheck.MESSAGE, 120 | ), sac); 121 | 122 | assertAnalyzerWarnings(q{ 123 | unittest { 124 | static assert(0, "foo bar"); 125 | static assert(0); /+ 126 | ^^^^^^^^^ [warn]: %s +/ 127 | } 128 | }c.format( 129 | AssertWithoutMessageCheck.MESSAGE, 130 | ), sac); 131 | 132 | // check for std.exception.enforce 133 | assertAnalyzerWarnings(q{ 134 | unittest { 135 | enforce(0); // std.exception not imported yet - could be a user-defined symbol 136 | import std.exception; 137 | enforce(0, "foo bar"); 138 | enforce(0); /+ 139 | ^^^^^^^^^^ [warn]: %s +/ 140 | } 141 | }c.format( 142 | AssertWithoutMessageCheck.MESSAGE, 143 | ), sac); 144 | 145 | // check for std.exception.enforce 146 | assertAnalyzerWarnings(q{ 147 | unittest { 148 | import exception; 149 | class C { 150 | import std.exception; 151 | } 152 | enforce(0); // std.exception not imported yet - could be a user-defined symbol 153 | struct S { 154 | import std.exception; 155 | } 156 | enforce(0); // std.exception not imported yet - could be a user-defined symbol 157 | if (false) { 158 | import std.exception; 159 | } 160 | enforce(0); // std.exception not imported yet - could be a user-defined symbol 161 | { 162 | import std.exception; 163 | } 164 | enforce(0); // std.exception not imported yet - could be a user-defined symbol 165 | } 166 | }c, sac); 167 | 168 | stderr.writeln("Unittest for AssertWithoutMessageCheck passed."); 169 | } 170 | -------------------------------------------------------------------------------- /src/dscanner/analysis/incorrect_infinite_range.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2015. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.incorrect_infinite_range; 7 | 8 | import dscanner.analysis.base; 9 | import dscanner.analysis.helpers; 10 | import dparse.ast; 11 | import dparse.lexer; 12 | 13 | import std.typecons : Rebindable; 14 | 15 | /** 16 | * Checks for incorrect infinite range definitions 17 | */ 18 | final class IncorrectInfiniteRangeCheck : BaseAnalyzer 19 | { 20 | alias visit = BaseAnalyzer.visit; 21 | 22 | mixin AnalyzerInfo!"incorrect_infinite_range_check"; 23 | 24 | /// 25 | this(BaseAnalyzerArguments args) 26 | { 27 | super(args); 28 | } 29 | 30 | override void visit(const StructBody structBody) 31 | { 32 | inStruct++; 33 | structBody.accept(this); 34 | inStruct--; 35 | } 36 | 37 | override void visit(const FunctionDeclaration fd) 38 | { 39 | if (inStruct > 0 && fd.name.text == "empty") 40 | { 41 | auto old = parentFunc; 42 | parentFunc = fd; 43 | fd.accept(this); 44 | parentFunc = old; 45 | } 46 | } 47 | 48 | override void visit(const FunctionBody fb) 49 | { 50 | if (fb.specifiedFunctionBody && fb.specifiedFunctionBody.blockStatement !is null) 51 | visit(fb.specifiedFunctionBody.blockStatement); 52 | else if (fb.shortenedFunctionBody && fb.shortenedFunctionBody.expression !is null) 53 | visitReturnExpression(fb.shortenedFunctionBody.expression); 54 | } 55 | 56 | override void visit(const BlockStatement bs) 57 | { 58 | if (bs.declarationsAndStatements is null) 59 | return; 60 | if (bs.declarationsAndStatements.declarationsAndStatements is null) 61 | return; 62 | if (bs.declarationsAndStatements.declarationsAndStatements.length != 1) 63 | return; 64 | visit(bs.declarationsAndStatements); 65 | } 66 | 67 | override void visit(const ReturnStatement rs) 68 | { 69 | if (inStruct == 0 || parentFunc == null) // not within a struct yet 70 | return; 71 | visitReturnExpression(rs.expression); 72 | } 73 | 74 | void visitReturnExpression(const Expression expression) 75 | { 76 | if (!expression || expression.items.length != 1) 77 | return; 78 | UnaryExpression unary = cast(UnaryExpression) expression.items[0]; 79 | if (unary is null) 80 | return; 81 | if (unary.primaryExpression is null) 82 | return; 83 | if (unary.primaryExpression.primary != tok!"false") 84 | return; 85 | addErrorMessage(parentFunc.get, KEY, MESSAGE); 86 | } 87 | 88 | override void visit(const Unittest u) 89 | { 90 | } 91 | 92 | private: 93 | uint inStruct; 94 | enum string KEY = "dscanner.suspicious.incorrect_infinite_range"; 95 | enum string MESSAGE = "Use `enum bool empty = false;` to define an infinite range."; 96 | Rebindable!(const FunctionDeclaration) parentFunc; 97 | } 98 | 99 | unittest 100 | { 101 | import std.stdio : stderr; 102 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 103 | import std.format : format; 104 | 105 | StaticAnalysisConfig sac = disabledConfig(); 106 | sac.incorrect_infinite_range_check = Check.enabled; 107 | assertAnalyzerWarnings(q{struct InfiniteRange 108 | { 109 | bool empty() 110 | { 111 | return false; 112 | } /+ 113 | ^^ [warn]: %1$s+/ 114 | // TODO: test for multiline issues like this 115 | 116 | bool stuff() 117 | { 118 | return false; 119 | } 120 | 121 | unittest 122 | { 123 | return false; 124 | } 125 | 126 | // https://issues.dlang.org/show_bug.cgi?id=18409 127 | struct Foo 128 | { 129 | ~this() nothrow @nogc; 130 | } 131 | } 132 | 133 | struct InfiniteRange 134 | { 135 | bool empty() => false; /+ 136 | ^^^^^^^^^^^^^^^^^^^^^^ [warn]: %1$s +/ 137 | bool stuff() => false; 138 | unittest 139 | { 140 | return false; 141 | } 142 | 143 | // https://issues.dlang.org/show_bug.cgi?id=18409 144 | struct Foo 145 | { 146 | ~this() nothrow @nogc; 147 | } 148 | } 149 | 150 | bool empty() { return false; } 151 | class C { bool empty() { return false; } } /+ 152 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [warn]: %1$s +/ 153 | 154 | }c 155 | .format(IncorrectInfiniteRangeCheck.MESSAGE), sac); 156 | } 157 | 158 | // test for https://github.com/dlang-community/D-Scanner/issues/656 159 | // unittests are skipped but D-Scanner sources are self checked 160 | version(none) struct Foo 161 | { 162 | void empty() 163 | { 164 | return; 165 | } 166 | } 167 | 168 | unittest 169 | { 170 | import std.stdio : stderr; 171 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 172 | import std.format : format; 173 | 174 | StaticAnalysisConfig sac = disabledConfig(); 175 | sac.incorrect_infinite_range_check = Check.enabled; 176 | assertAnalyzerWarnings(q{ 177 | enum isAllZeroBits = () 178 | { 179 | if (true) 180 | return true; 181 | else 182 | return false; 183 | }(); 184 | }, sac); 185 | stderr.writeln("Unittest for IncorrectInfiniteRangeCheck passed."); 186 | } 187 | -------------------------------------------------------------------------------- /src/dscanner/analysis/range.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.range; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dscanner.analysis.base; 12 | import dscanner.analysis.helpers; 13 | import dsymbol.scope_ : Scope; 14 | 15 | /** 16 | * Checks for .. expressions where the left side is larger than the right. This 17 | * is almost always a mistake. 18 | */ 19 | final class BackwardsRangeCheck : BaseAnalyzer 20 | { 21 | alias visit = BaseAnalyzer.visit; 22 | 23 | mixin AnalyzerInfo!"backwards_range_check"; 24 | 25 | /// Key for this check in the report output 26 | enum string KEY = "dscanner.bugs.backwards_slices"; 27 | 28 | /** 29 | * Params: 30 | * fileName = the name of the file being analyzed 31 | */ 32 | this(BaseAnalyzerArguments args) 33 | { 34 | super(args); 35 | } 36 | 37 | override void visit(const ForeachStatement foreachStatement) 38 | { 39 | if (foreachStatement.low !is null && foreachStatement.high !is null) 40 | { 41 | import std.string : format; 42 | 43 | state = State.left; 44 | visit(foreachStatement.low); 45 | state = State.right; 46 | visit(foreachStatement.high); 47 | state = State.ignore; 48 | if (hasLeft && hasRight && left > right) 49 | { 50 | string message = format( 51 | "%d is larger than %d. Did you mean to use 'foreach_reverse( ... ; %d .. %d)'?", 52 | left, right, right, left); 53 | auto start = &foreachStatement.low.tokens[0]; 54 | auto endIncl = &foreachStatement.high.tokens[$ - 1]; 55 | assert(endIncl >= start); 56 | auto tokens = start[0 .. endIncl - start + 1]; 57 | addErrorMessage(tokens, KEY, message); 58 | } 59 | hasLeft = false; 60 | hasRight = false; 61 | } 62 | foreachStatement.accept(this); 63 | } 64 | 65 | override void visit(const AddExpression add) 66 | { 67 | immutable s = state; 68 | state = State.ignore; 69 | add.accept(this); 70 | state = s; 71 | } 72 | 73 | override void visit(const UnaryExpression unary) 74 | { 75 | if (state != State.ignore && unary.primaryExpression is null) 76 | return; 77 | else 78 | unary.accept(this); 79 | } 80 | 81 | override void visit(const PrimaryExpression primary) 82 | { 83 | import std.conv : to, ConvException; 84 | 85 | if (state == State.ignore || !isNumberLiteral(primary.primary.type)) 86 | return; 87 | if (state == State.left) 88 | { 89 | try 90 | left = parseNumber(primary.primary.text); 91 | catch (ConvException e) 92 | return; 93 | hasLeft = true; 94 | } 95 | else 96 | { 97 | try 98 | right = parseNumber(primary.primary.text); 99 | catch (ConvException e) 100 | return; 101 | hasRight = true; 102 | } 103 | } 104 | 105 | override void visit(const Index index) 106 | { 107 | if (index.low !is null && index.high !is null) 108 | { 109 | state = State.left; 110 | dynamicDispatch(index.low); 111 | state = State.right; 112 | dynamicDispatch(index.high); 113 | state = State.ignore; 114 | if (hasLeft && hasRight && left > right) 115 | { 116 | import std.string : format; 117 | 118 | string message = format("%d is larger than %d. This slice is likely incorrect.", 119 | left, right); 120 | addErrorMessage(index, KEY, message); 121 | } 122 | hasLeft = false; 123 | hasRight = false; 124 | } 125 | index.accept(this); 126 | } 127 | 128 | private: 129 | bool hasLeft; 130 | bool hasRight; 131 | long left; 132 | long right; 133 | enum State 134 | { 135 | ignore, 136 | left, 137 | right 138 | } 139 | 140 | State state = State.ignore; 141 | 142 | long parseNumber(string te) 143 | { 144 | import std.conv : to; 145 | import std.regex : ctRegex, replaceAll; 146 | 147 | enum re = ctRegex!("[_uUlL]", ""); 148 | string t = te.replaceAll(re, ""); 149 | if (t.length > 2) 150 | { 151 | if (t[1] == 'x' || t[1] == 'X') 152 | return to!long(t[2 .. $], 16); 153 | if (t[1] == 'b' || t[1] == 'B') 154 | return to!long(t[2 .. $], 2); 155 | } 156 | return to!long(t); 157 | } 158 | } 159 | 160 | unittest 161 | { 162 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 163 | 164 | StaticAnalysisConfig sac = disabledConfig(); 165 | sac.backwards_range_check = Check.enabled; 166 | assertAnalyzerWarnings(q{ 167 | void testRange() 168 | { 169 | a = node.tupleof[2..T.length+1]; // ok 170 | foreach (a; 10 .. j + 2) {} // ok 171 | 172 | int[] data = [1, 2, 3, 4, 5]; 173 | 174 | data = data[1 .. 3]; // ok 175 | data = data[3 .. 1]; /+ 176 | ^^^^^^ [warn]: 3 is larger than 1. This slice is likely incorrect. +/ 177 | 178 | foreach (n; 1 .. 3) { } // ok 179 | foreach (n; 3 .. 1) { } /+ 180 | ^^^^^^ [warn]: 3 is larger than 1. Did you mean to use 'foreach_reverse( ... ; 1 .. 3)'? +/ 181 | } 182 | }c, sac); 183 | 184 | stderr.writeln("Unittest for BackwardsRangeCheck passed."); 185 | } 186 | -------------------------------------------------------------------------------- /tests/dscanner.ini: -------------------------------------------------------------------------------- 1 | ; Configure which static analysis checks are enabled 2 | [analysis.config.StaticAnalysisConfig] 3 | ; Check variable, class, struct, interface, union, and function names against the Phobos style guide 4 | style_check="enabled" 5 | ; Check for array literals that cause unnecessary allocation 6 | enum_array_literal_check="enabled" 7 | ; Check for poor exception handling practices 8 | exception_check="enabled" 9 | ; Check for use of the deprecated 'delete' keyword 10 | delete_check="enabled" 11 | ; Check for use of the deprecated floating point operators 12 | float_operator_check="enabled" 13 | ; Check number literals for readability 14 | number_style_check="enabled" 15 | ; Checks that opEquals, opCmp, toHash, and toString are either const, immutable, or inout. 16 | object_const_check="enabled" 17 | ; Checks for .. expressions where the left side is larger than the right. 18 | backwards_range_check="enabled" 19 | ; Checks for if statements whose 'then' block is the same as the 'else' block 20 | if_else_same_check="enabled" 21 | ; Checks for some problems with constructors 22 | constructor_check="enabled" 23 | ; Checks for unused variables 24 | unused_variable_check="enabled" 25 | ; Checks for unused labels 26 | unused_label_check="enabled" 27 | ; Checks for unused function parameters 28 | unused_parameter_check="enabled" 29 | ; Checks for duplicate attributes 30 | duplicate_attribute="enabled" 31 | ; Checks that opEquals and toHash are both defined or neither are defined 32 | opequals_tohash_check="enabled" 33 | ; Checks for subtraction from .length properties 34 | length_subtraction_check="enabled" 35 | ; Checks for methods or properties whose names conflict with built-in properties 36 | builtin_property_names_check="enabled" 37 | ; Checks for confusing code in inline asm statements 38 | asm_style_check="enabled" 39 | ; Checks for confusing logical operator precedence 40 | logical_precedence_check="enabled" 41 | ; Checks for undocumented public declarations 42 | undocumented_declaration_check="disabled" 43 | ; Checks for poor placement of function attributes 44 | function_attribute_check="enabled" 45 | ; Checks for use of the comma operator 46 | comma_expression_check="enabled" 47 | ; Checks for local imports that are too broad. Only accurate when checking code used with D versions older than 2.071.0 48 | local_import_check="enabled" 49 | ; Checks for variables that could be declared immutable 50 | could_be_immutable_check="enabled" 51 | ; Checks for redundant expressions in if statements 52 | redundant_if_check="enabled" 53 | ; Checks for redundant parenthesis 54 | redundant_parens_check="enabled" 55 | ; Checks for mismatched argument and parameter names 56 | mismatched_args_check="enabled" 57 | ; Checks for labels with the same name as variables 58 | label_var_same_name_check="enabled" 59 | ; Checks for lines longer than `max_line_length` characters 60 | long_line_check="enabled" 61 | ; Checks for assignment to auto-ref function parameters 62 | auto_ref_assignment_check="enabled" 63 | ; Checks for incorrect infinite range definitions 64 | incorrect_infinite_range_check="enabled" 65 | ; Checks for asserts that are always true 66 | useless_assert_check="enabled" 67 | ; Check for uses of the old-style alias syntax 68 | alias_syntax_check="enabled" 69 | ; Checks for else if that should be else static if 70 | static_if_else_check="enabled" 71 | ; Check for unclear lambda syntax 72 | lambda_return_check="enabled" 73 | ; Check for auto function without return statement 74 | auto_function_check="enabled" 75 | ; Check for sortedness of imports 76 | imports_sortedness="enabled" 77 | ; Check for explicitly annotated unittests 78 | explicitly_annotated_unittests="enabled" 79 | ; Check for properly documented public functions (Returns, Params) 80 | properly_documented_public_functions="enabled" 81 | ; Check for useless usage of the final attribute 82 | final_attribute_check="enabled" 83 | ; Check for virtual calls in the class constructors 84 | vcall_in_ctor="enabled" 85 | ; Check for useless user defined initializers 86 | useless_initializer="enabled" 87 | ; Check allman brace style 88 | allman_braces_check="enabled" 89 | ; Check for redundant attributes 90 | redundant_attributes_check="enabled" 91 | ; Check public declarations without a documented unittest 92 | has_public_example="enabled" 93 | ; Check for asserts without an explanatory message 94 | assert_without_msg="enabled" 95 | ; Check indent of if constraints 96 | if_constraints_indent="enabled" 97 | ; Check for @trusted applied to a bigger scope than a single function 98 | trust_too_much="enabled" 99 | ; Check for redundant storage classes on variable declarations 100 | redundant_storage_classes="enabled" 101 | ; Check for unused function return values 102 | unused_result="enabled" 103 | ; Enable cyclomatic complexity check 104 | cyclomatic_complexity="enabled" 105 | ; Check for function bodies on discord functions 106 | body_on_disabled_func_check="enabled" 107 | ; Formatting brace style for automatic fixes (allman, otbs, stroustrup, knr) 108 | brace_style="allman" 109 | ; Formatting indentation style for automatic fixes (tabs, spaces) 110 | indentation_style="tab" 111 | ; Formatting line ending character (lf, cr, crlf) 112 | eol_style="lf" 113 | 114 | -------------------------------------------------------------------------------- /src/dscanner/analysis/objectconst.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.objectconst; 7 | 8 | import std.stdio; 9 | import std.regex; 10 | import dparse.ast; 11 | import dparse.lexer; 12 | import dscanner.analysis.base; 13 | import dscanner.analysis.helpers; 14 | import dsymbol.scope_ : Scope; 15 | 16 | /** 17 | * Checks that opEquals, opCmp, toHash, 'opCast', and toString are either const, 18 | * immutable, or inout. 19 | */ 20 | final class ObjectConstCheck : BaseAnalyzer 21 | { 22 | alias visit = BaseAnalyzer.visit; 23 | 24 | mixin AnalyzerInfo!"object_const_check"; 25 | 26 | /// 27 | this(BaseAnalyzerArguments args) 28 | { 29 | super(args); 30 | } 31 | 32 | mixin visitTemplate!ClassDeclaration; 33 | mixin visitTemplate!InterfaceDeclaration; 34 | mixin visitTemplate!UnionDeclaration; 35 | mixin visitTemplate!StructDeclaration; 36 | 37 | override void visit(const AttributeDeclaration d) 38 | { 39 | if (d.attribute.attribute == tok!"const" && inAggregate) 40 | { 41 | constColon = true; 42 | } 43 | d.accept(this); 44 | } 45 | 46 | override void visit(const Declaration d) 47 | { 48 | import std.algorithm : any; 49 | bool setConstBlock; 50 | if (inAggregate && d.attributes && d.attributes.any!(a => a.attribute == tok!"const")) 51 | { 52 | setConstBlock = true; 53 | constBlock = true; 54 | } 55 | 56 | bool containsDisable(A)(const A[] attribs) 57 | { 58 | import std.algorithm.searching : canFind; 59 | return attribs.canFind!(a => a.atAttribute !is null && 60 | a.atAttribute.identifier.text == "disable"); 61 | } 62 | 63 | if (const FunctionDeclaration fd = d.functionDeclaration) 64 | { 65 | const isDeclationDisabled = containsDisable(d.attributes) || 66 | containsDisable(fd.memberFunctionAttributes); 67 | 68 | if (inAggregate && !constColon && !constBlock && !isDeclationDisabled 69 | && isInteresting(fd.name.text) && !hasConst(fd.memberFunctionAttributes)) 70 | { 71 | addErrorMessage(d.functionDeclaration.name, KEY, 72 | "Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const."); 73 | } 74 | } 75 | 76 | d.accept(this); 77 | 78 | if (!inAggregate) 79 | constColon = false; 80 | if (setConstBlock) 81 | constBlock = false; 82 | } 83 | 84 | private: 85 | 86 | enum string KEY = "dscanner.suspicious.object_const"; 87 | 88 | static bool hasConst(const MemberFunctionAttribute[] attributes) 89 | { 90 | import std.algorithm : any; 91 | 92 | return attributes.any!(a => a.tokenType == tok!"const" 93 | || a.tokenType == tok!"immutable" || a.tokenType == tok!"inout"); 94 | } 95 | 96 | static bool isInteresting(string name) 97 | { 98 | return name == "opCmp" || name == "toHash" || name == "opEquals" 99 | || name == "toString" || name == "opCast"; 100 | } 101 | 102 | bool constBlock; 103 | bool constColon; 104 | } 105 | 106 | unittest 107 | { 108 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 109 | 110 | StaticAnalysisConfig sac = disabledConfig(); 111 | sac.object_const_check = Check.enabled; 112 | assertAnalyzerWarnings(q{ 113 | void testConsts() 114 | { 115 | // Will be ok because all are declared const/immutable 116 | class Cat 117 | { 118 | const bool opEquals(Object a, Object b) // ok 119 | { 120 | return true; 121 | } 122 | 123 | const int opCmp(Object o) // ok 124 | { 125 | return 1; 126 | } 127 | 128 | const hash_t toHash() // ok 129 | { 130 | return 0; 131 | } 132 | 133 | const string toString() // ok 134 | { 135 | return "Cat"; 136 | } 137 | } 138 | 139 | class Bat 140 | { 141 | const: override string toString() { return "foo"; } // ok 142 | } 143 | 144 | class Fox 145 | { 146 | const{ override string toString() { return "foo"; }} // ok 147 | } 148 | 149 | class Rat 150 | { 151 | bool opEquals(Object a, Object b) @disable; // ok 152 | } 153 | 154 | class Ant 155 | { 156 | @disable bool opEquals(Object a, Object b); // ok 157 | } 158 | 159 | // Will warn, because none are const 160 | class Dog 161 | { 162 | bool opEquals(Object a, Object b) /+ 163 | ^^^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ 164 | { 165 | return true; 166 | } 167 | 168 | int opCmp(Object o) /+ 169 | ^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ 170 | { 171 | return 1; 172 | } 173 | 174 | hash_t toHash() /+ 175 | ^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ 176 | { 177 | return 0; 178 | } 179 | 180 | string toString() /+ 181 | ^^^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ 182 | { 183 | return "Dog"; 184 | } 185 | } 186 | } 187 | }c, sac); 188 | 189 | stderr.writeln("Unittest for ObjectConstCheck passed."); 190 | } 191 | -------------------------------------------------------------------------------- /src/dscanner/analysis/opequals_without_tohash.d: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, Matthew Brennan Jones 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.opequals_without_tohash; 7 | 8 | import dparse.ast; 9 | import dparse.lexer; 10 | import dscanner.analysis.base; 11 | import dscanner.analysis.helpers; 12 | import dsymbol.scope_ : Scope; 13 | import std.stdio; 14 | import std.typecons : Rebindable; 15 | 16 | /** 17 | * Checks for when a class/struct has the method opEquals without toHash, or 18 | * toHash without opEquals. 19 | */ 20 | final class OpEqualsWithoutToHashCheck : BaseAnalyzer 21 | { 22 | alias visit = BaseAnalyzer.visit; 23 | 24 | mixin AnalyzerInfo!"opequals_tohash_check"; 25 | 26 | this(BaseAnalyzerArguments args) 27 | { 28 | super(args); 29 | } 30 | 31 | override void visit(const ClassDeclaration node) 32 | { 33 | actualCheck(node.name, node.structBody); 34 | node.accept(this); 35 | } 36 | 37 | override void visit(const StructDeclaration node) 38 | { 39 | actualCheck(node.name, node.structBody); 40 | node.accept(this); 41 | } 42 | 43 | private void actualCheck(const Token name, const StructBody structBody) 44 | { 45 | Rebindable!(const Declaration) hasOpEquals; 46 | Rebindable!(const Declaration) hasToHash; 47 | Rebindable!(const Declaration) hasOpCmp; 48 | 49 | // Just return if missing children 50 | if (!structBody || !structBody.declarations || name is Token.init) 51 | return; 52 | 53 | // Check all the function declarations 54 | foreach (declaration; structBody.declarations) 55 | { 56 | // Skip if not a function declaration 57 | if (!declaration || !declaration.functionDeclaration) 58 | continue; 59 | 60 | bool containsDisable(A)(const A[] attribs) 61 | { 62 | import std.algorithm.searching : canFind; 63 | return attribs.canFind!(a => a.atAttribute !is null && 64 | a.atAttribute.identifier.text == "disable"); 65 | } 66 | 67 | const isDeclationDisabled = containsDisable(declaration.attributes) || 68 | containsDisable(declaration.functionDeclaration.memberFunctionAttributes); 69 | 70 | if (isDeclationDisabled) 71 | continue; 72 | 73 | // Check if opEquals or toHash 74 | immutable string methodName = declaration.functionDeclaration.name.text; 75 | if (methodName == "opEquals") 76 | hasOpEquals = declaration; 77 | else if (methodName == "toHash") 78 | hasToHash = declaration; 79 | else if (methodName == "opCmp") 80 | hasOpCmp = declaration; 81 | } 82 | 83 | // Warn if has opEquals, but not toHash 84 | if (hasOpEquals && !hasToHash) 85 | { 86 | string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'."; 87 | addErrorMessage( 88 | Message.Diagnostic.from(fileName, name, message), 89 | [ 90 | Message.Diagnostic.from(fileName, hasOpEquals.get, "'opEquals' defined here") 91 | ], 92 | KEY 93 | ); 94 | } 95 | // Warn if has toHash, but not opEquals 96 | else if (!hasOpEquals && hasToHash) 97 | { 98 | string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'."; 99 | addErrorMessage( 100 | Message.Diagnostic.from(fileName, name, message), 101 | [ 102 | Message.Diagnostic.from(fileName, hasToHash.get, "'toHash' defined here") 103 | ], 104 | KEY 105 | ); 106 | } 107 | } 108 | 109 | enum string KEY = "dscanner.suspicious.incomplete_operator_overloading"; 110 | } 111 | 112 | unittest 113 | { 114 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 115 | 116 | StaticAnalysisConfig sac = disabledConfig(); 117 | sac.opequals_tohash_check = Check.enabled; 118 | // TODO: test supplemental diagnostics 119 | assertAnalyzerWarnings(q{ 120 | // Success because it has opEquals and toHash 121 | class Chimp 122 | { 123 | const bool opEquals(Object a, Object b) 124 | { 125 | return true; 126 | } 127 | 128 | const override hash_t toHash() 129 | { 130 | return 0; 131 | } 132 | } 133 | 134 | // AA would use default equal and default toHash 135 | struct Bee 136 | { 137 | int opCmp(Bee) const 138 | { 139 | return true; 140 | } 141 | } 142 | 143 | // Fail on class opEquals 144 | class Rabbit /+ 145 | ^^^^^^ [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'. +/ 146 | { 147 | const bool opEquals(Object a, Object b) 148 | { 149 | return true; 150 | } 151 | } 152 | 153 | // Fail on class toHash 154 | class Kangaroo /+ 155 | ^^^^^^^^ [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'. +/ 156 | { 157 | override const hash_t toHash() 158 | { 159 | return 0; 160 | } 161 | } 162 | 163 | // Fail on struct opEquals 164 | struct Tarantula /+ 165 | ^^^^^^^^^ [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'. +/ 166 | { 167 | const bool opEquals(Object a, Object b) 168 | { 169 | return true; 170 | } 171 | } 172 | 173 | // Fail on struct toHash 174 | struct Puma /+ 175 | ^^^^ [warn]: 'Puma' has method 'toHash', but not 'opEquals'. +/ 176 | { 177 | const nothrow @safe hash_t toHash() 178 | { 179 | return 0; 180 | } 181 | } 182 | 183 | // issue #659, do not warn if one miss and the other is not callable 184 | struct Fox {const nothrow @safe hash_t toHash() @disable;} 185 | struct Bat {@disable const nothrow @safe hash_t toHash();} 186 | struct Rat {const bool opEquals(Object a, Object b) @disable;} 187 | struct Cat {@disable const bool opEquals(Object a, Object b);} 188 | 189 | }c, sac); 190 | 191 | stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed."); 192 | } 193 | -------------------------------------------------------------------------------- /src/dscanner/analysis/unused_result.d: -------------------------------------------------------------------------------- 1 | // Copyright Vladimir Panteleev 2020 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | module dscanner.analysis.unused_result; 6 | 7 | import dparse.ast; 8 | import dparse.lexer; 9 | import dscanner.analysis.base; 10 | import dscanner.analysis.mismatched_args : IdentVisitor, resolveSymbol; 11 | import dscanner.utils; 12 | import dsymbol.scope_; 13 | import dsymbol.symbol; 14 | import std.algorithm : canFind, countUntil; 15 | import std.range : retro; 16 | 17 | /** 18 | * Checks for function call statements which call non-void functions. 19 | * 20 | * In case the function returns a value indicating success/failure, 21 | * ignoring this return value and continuing execution can lead to 22 | * undesired results. 23 | * 24 | * When the return value is intentionally discarded, `cast(void)` can 25 | * be prepended to silence the check. 26 | */ 27 | final class UnusedResultChecker : BaseAnalyzer 28 | { 29 | alias visit = BaseAnalyzer.visit; 30 | 31 | mixin AnalyzerInfo!"unused_result"; 32 | 33 | private: 34 | 35 | enum string KEY = "dscanner.unused_result"; 36 | enum string MSG = "Function return value is discarded"; 37 | 38 | public: 39 | 40 | const(DSymbol)* void_; 41 | const(DSymbol)* noreturn_; 42 | 43 | /// 44 | this(BaseAnalyzerArguments args) 45 | { 46 | super(args); 47 | void_ = sc.getSymbolsByName(internString("void"))[0]; 48 | auto symbols = sc.getSymbolsByName(internString("noreturn")); 49 | if (symbols.length > 0) 50 | noreturn_ = symbols[0]; 51 | } 52 | 53 | override void visit(const(ExpressionStatement) decl) 54 | { 55 | import std.typecons : scoped; 56 | 57 | super.visit(decl); 58 | if (!decl.expression) 59 | return; 60 | if (decl.expression.items.length != 1) 61 | return; 62 | auto ue = cast(UnaryExpression) decl.expression.items[0]; 63 | if (!ue) 64 | return; 65 | auto fce = ue.functionCallExpression; 66 | if (!fce) 67 | return; 68 | 69 | auto identVisitor = scoped!IdentVisitor; 70 | if (fce.unaryExpression !is null) 71 | identVisitor.visit(fce.unaryExpression); 72 | else if (fce.type !is null) 73 | identVisitor.visit(fce.type); 74 | 75 | if (!identVisitor.names.length) 76 | return; 77 | 78 | const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names); 79 | 80 | if (!symbols.length) 81 | return; 82 | 83 | foreach (sym; symbols) 84 | { 85 | if (!sym) 86 | return; 87 | if (!sym.type) 88 | return; 89 | if (sym.kind != CompletionKind.functionName) 90 | return; 91 | if (sym.type is void_) 92 | return; 93 | if (noreturn_ && sym.type is noreturn_) 94 | return; 95 | } 96 | 97 | const(Token)[] tokens = fce.unaryExpression 98 | ? fce.unaryExpression.tokens 99 | : fce.type 100 | ? fce.type.tokens 101 | : fce.tokens; 102 | 103 | addErrorMessage(tokens, KEY, MSG); 104 | } 105 | } 106 | 107 | unittest 108 | { 109 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 110 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 111 | import std.stdio : stderr; 112 | import std.format : format; 113 | 114 | StaticAnalysisConfig sac = disabledConfig(); 115 | sac.unused_result = Check.enabled; 116 | 117 | assertAnalyzerWarnings(q{ 118 | void fun() {} 119 | void main() 120 | { 121 | fun(); 122 | } 123 | }c, sac); 124 | 125 | assertAnalyzerWarnings(q{ 126 | alias noreturn = typeof(*null); 127 | noreturn fun() { while (1) {} } 128 | noreturn main() 129 | { 130 | fun(); 131 | } 132 | }c, sac); 133 | 134 | assertAnalyzerWarnings(q{ 135 | int fun() { return 1; } 136 | void main() 137 | { 138 | fun(); /+ 139 | ^^^ [warn]: %s +/ 140 | } 141 | }c.format(UnusedResultChecker.MSG), sac); 142 | 143 | assertAnalyzerWarnings(q{ 144 | struct Foo 145 | { 146 | static bool get() 147 | { 148 | return false; 149 | } 150 | } 151 | alias Bar = Foo; 152 | void main() 153 | { 154 | Bar.get(); /+ 155 | ^^^^^^^ [warn]: %s +/ 156 | } 157 | }c.format(UnusedResultChecker.MSG), sac); 158 | 159 | assertAnalyzerWarnings(q{ 160 | void main() 161 | { 162 | void fun() {} 163 | fun(); 164 | } 165 | }c, sac); 166 | 167 | version (none) // TODO: local functions 168 | assertAnalyzerWarnings(q{ 169 | void main() 170 | { 171 | int fun() { return 1; } 172 | fun(); /+ 173 | ^^^ [warn]: %s +/ 174 | } 175 | }c.format(UnusedResultChecker.MSG), sac); 176 | 177 | assertAnalyzerWarnings(q{ 178 | int fun() { return 1; } 179 | void main() 180 | { 181 | cast(void) fun(); 182 | } 183 | }c, sac); 184 | 185 | assertAnalyzerWarnings(q{ 186 | void fun() { } 187 | alias gun = fun; 188 | void main() 189 | { 190 | gun(); 191 | } 192 | }c, sac); 193 | 194 | import std.stdio: writeln; 195 | writeln("Unittest for UnusedResultChecker passed"); 196 | } 197 | 198 | -------------------------------------------------------------------------------- /src/dscanner/analysis/always_curly.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.always_curly; 6 | 7 | import dparse.lexer; 8 | import dparse.ast; 9 | import dscanner.analysis.base; 10 | import dsymbol.scope_ : Scope; 11 | 12 | import std.array : back, front; 13 | import std.algorithm; 14 | import std.range; 15 | import std.stdio; 16 | 17 | final class AlwaysCurlyCheck : BaseAnalyzer 18 | { 19 | mixin AnalyzerInfo!"always_curly_check"; 20 | 21 | alias visit = BaseAnalyzer.visit; 22 | 23 | /// 24 | this(BaseAnalyzerArguments args) 25 | { 26 | super(args); 27 | } 28 | 29 | void test(L, B)(L loc, B s, string stmtKind) 30 | { 31 | if (!is(s == BlockStatement)) 32 | { 33 | if (!s.tokens.empty) 34 | { 35 | AutoFix af = AutoFix.insertionBefore(s.tokens.front, " { ") 36 | .concat(AutoFix.insertionAfter(s.tokens.back, " } ")); 37 | af.name = "Wrap in braces"; 38 | 39 | addErrorMessage(loc, KEY, stmtKind ~ MESSAGE_POSTFIX, [af]); 40 | } 41 | else 42 | { 43 | addErrorMessage(loc, KEY, stmtKind ~ MESSAGE_POSTFIX); 44 | } 45 | } 46 | } 47 | 48 | override void visit(const(IfStatement) stmt) 49 | { 50 | auto s = stmt.thenStatement.statement; 51 | this.test(stmt.thenStatement, s, "if"); 52 | if (stmt.elseStatement !is null) 53 | { 54 | auto e = stmt.elseStatement.statement; 55 | this.test(stmt.elseStatement, e, "else"); 56 | } 57 | } 58 | 59 | override void visit(const(ForStatement) stmt) 60 | { 61 | auto s = stmt.declarationOrStatement; 62 | if (s.statement !is null) 63 | { 64 | this.test(s, s, "for"); 65 | } 66 | } 67 | 68 | override void visit(const(ForeachStatement) stmt) 69 | { 70 | auto s = stmt.declarationOrStatement; 71 | if (s.statement !is null) 72 | { 73 | this.test(s, s, "foreach"); 74 | } 75 | } 76 | 77 | override void visit(const(TryStatement) stmt) 78 | { 79 | auto s = stmt.declarationOrStatement; 80 | if (s.statement !is null) 81 | { 82 | this.test(s, s, "try"); 83 | } 84 | 85 | if (stmt.catches !is null) 86 | { 87 | foreach (const(Catch) ct; stmt.catches.catches) 88 | { 89 | this.test(ct, ct.declarationOrStatement, "catch"); 90 | } 91 | if (stmt.catches.lastCatch !is null) 92 | { 93 | auto sncnd = stmt.catches.lastCatch.statementNoCaseNoDefault; 94 | if (sncnd !is null) 95 | { 96 | this.test(stmt.catches.lastCatch, sncnd, "finally"); 97 | } 98 | } 99 | } 100 | } 101 | 102 | override void visit(const(WhileStatement) stmt) 103 | { 104 | auto s = stmt.declarationOrStatement; 105 | if (s.statement !is null) 106 | { 107 | this.test(s, s, "while"); 108 | } 109 | } 110 | 111 | override void visit(const(DoStatement) stmt) 112 | { 113 | auto s = stmt.statementNoCaseNoDefault; 114 | if (s !is null) 115 | { 116 | this.test(s, s, "do"); 117 | } 118 | } 119 | 120 | enum string KEY = "dscanner.style.always_curly"; 121 | enum string MESSAGE_POSTFIX = " must be follow by a BlockStatement aka. { }"; 122 | } 123 | 124 | unittest 125 | { 126 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 127 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 128 | import std.stdio : stderr; 129 | 130 | StaticAnalysisConfig sac = disabledConfig(); 131 | sac.always_curly_check = Check.enabled; 132 | 133 | assertAnalyzerWarnings(q{ 134 | void testIf() 135 | { 136 | if(true) return; // [warn]: if must be follow by a BlockStatement aka. { } 137 | } 138 | }, sac); 139 | 140 | assertAnalyzerWarnings(q{ 141 | void testIf() 142 | { 143 | if(true) return; /+ 144 | ^^^^^^^ [warn]: if must be follow by a BlockStatement aka. { } +/ 145 | } 146 | }, sac); 147 | 148 | assertAnalyzerWarnings(q{ 149 | void testIf() 150 | { 151 | for(int i = 0; i < 10; ++i) return; // [warn]: for must be follow by a BlockStatement aka. { } 152 | } 153 | }, sac); 154 | 155 | assertAnalyzerWarnings(q{ 156 | void testIf() 157 | { 158 | foreach(it; 0 .. 10) return; // [warn]: foreach must be follow by a BlockStatement aka. { } 159 | } 160 | }, sac); 161 | 162 | assertAnalyzerWarnings(q{ 163 | void testIf() 164 | { 165 | while(true) return; // [warn]: while must be follow by a BlockStatement aka. { } 166 | } 167 | }, sac); 168 | 169 | assertAnalyzerWarnings(q{ 170 | void testIf() 171 | { 172 | do return; while(true); return; // [warn]: do must be follow by a BlockStatement aka. { } 173 | } 174 | }, sac); 175 | } 176 | 177 | unittest { 178 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 179 | import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; 180 | import std.stdio : stderr; 181 | 182 | StaticAnalysisConfig sac = disabledConfig(); 183 | sac.always_curly_check = Check.enabled; 184 | 185 | assertAutoFix(q{ 186 | void test() { 187 | if(true) return; // fix:0 188 | } 189 | }c, q{ 190 | void test() { 191 | if(true) { return; } // fix:0 192 | } 193 | }c, sac); 194 | 195 | assertAutoFix(q{ 196 | void test() { 197 | foreach(_; 0 .. 10 ) return; // fix:0 198 | } 199 | }c, q{ 200 | void test() { 201 | foreach(_; 0 .. 10 ) { return; } // fix:0 202 | } 203 | }c, sac); 204 | 205 | assertAutoFix(q{ 206 | void test() { 207 | for(int i = 0; i < 10; ++i) return; // fix:0 208 | } 209 | }c, q{ 210 | void test() { 211 | for(int i = 0; i < 10; ++i) { return; } // fix:0 212 | } 213 | }c, sac); 214 | 215 | assertAutoFix(q{ 216 | void test() { 217 | do return; while(true) // fix:0 218 | } 219 | }c, q{ 220 | void test() { 221 | do { return; } while(true) // fix:0 222 | } 223 | }c, sac); 224 | 225 | 226 | stderr.writeln("Unittest for AlwaysCurly passed."); 227 | } 228 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | pull_request: 5 | 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | main: 12 | name: Run all tests 13 | 14 | # Only run for the main repository - not forks 15 | if: ${{ github.repository == 'dlang-community/D-Scanner' }} 16 | 17 | # Run permutations of common os + host compilers 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | compiler: [ 22 | { 23 | version: dmd-latest, 24 | dmd: dmd 25 | }, 26 | { 27 | version: ldc-latest, 28 | dmd: ldmd2 29 | }, 30 | { 31 | # Install dmd for the associated tools (dub/rdmd) 32 | version: dmd-latest, 33 | dmd: gdc-12 34 | } 35 | ] 36 | host: [ 37 | ubuntu-22.04, 38 | macos-latest, 39 | windows-latest 40 | ] 41 | build: [ 42 | { type: make }, 43 | { type: dub, version: 'current' }, 44 | { type: dub, version: 'min libdparse' }, 45 | # Fail due to unresolvable dependencies 46 | # { type: dub, version: 'max libdparse' }, 47 | # { type: dub, version: 'min dsymbol' }, 48 | # { type: dub, version: 'max dsymbol' }, 49 | ] 50 | 51 | exclude: 52 | # Restrict GDC to Ubuntu 53 | - compiler: 54 | dmd: gdc-12 55 | host: windows-latest 56 | - compiler: 57 | dmd: gdc-12 58 | host: macos-latest 59 | 60 | # Restrict DMD to macOS latest 61 | - compiler: 62 | dmd: dmd 63 | host: macos-latest 64 | 65 | # Omit dub builds for GDC because dub rejects the old fronted revision 66 | - compiler: 67 | dmd: gdc-12 68 | build: 69 | type: dub 70 | include: 71 | - { do_report: 1, build: { type: dub, version: 'current' }, host: 'ubuntu-22.04', compiler: { version: dmd-latest, dmd: dmd } } 72 | 73 | - compiler: 74 | dmd: dmd 75 | host: macos-13 76 | build: 77 | type: 'dub' 78 | version: 'current' 79 | 80 | runs-on: ${{ matrix.host }} 81 | 82 | steps: 83 | # Clone repo + submodules 84 | - name: Checkout repo 85 | uses: actions/checkout@v4 86 | with: 87 | submodules: 'recursive' 88 | fetch-depth: 0 89 | 90 | # Install the host compiler (DMD or LDC) 91 | # Also grabs DMD for GDC to include dub + rdmd 92 | - name: Install ${{ matrix.compiler.version }} 93 | if: ${{ matrix.compiler.dmd != 'gdc-12' || matrix.build.type == 'dub' }} # Fetch required tools for GDC 94 | uses: dlang-community/setup-dlang@v1 95 | with: 96 | compiler: ${{ matrix.compiler.version }} 97 | 98 | # GDC not yet supported by setup-dlang 99 | - name: Install GDC via apt-get 100 | if: ${{ matrix.compiler.dmd == 'gdc-12' }} 101 | run: | 102 | sudo apt-get install gdc-12 -y 103 | gdc-12 --version 104 | 105 | # Compile D-Scanner and execute all tests without dub 106 | - name: Build and test without dub 107 | if: ${{ matrix.build.type == 'make' }} 108 | env: 109 | DC: ${{ matrix.compiler.dmd }} 110 | shell: bash 111 | run: | 112 | if [ "$RUNNER_OS" == "Windows" ]; then 113 | export MFLAGS="-m64" 114 | ./build.bat 115 | ./build.bat test 116 | else 117 | make "-j$(nproc)" all test 118 | fi 119 | 120 | # Compile D-Scanner and execute all tests using a specific dependency version 121 | # Currently skipped for GDC (dub installed from apt-get is broken) 122 | - name: Build and test with dub (min or max libdparse test) 123 | if: ${{ matrix.build.type == 'dub' && matrix.build.version != 'current' }} 124 | env: 125 | DC: ${{ matrix.compiler.dmd }} 126 | run: | 127 | rdmd ./d-test-utils/test_with_package.d ${{ matrix.build.version }} -- dub build 128 | rdmd ./d-test-utils/test_with_package.d ${{ matrix.build.version }} -- dub test 129 | 130 | - name: Build and test with dub (with dub.selections.json) 131 | if: ${{ matrix.build.type == 'dub' && matrix.build.version == 'current' }} 132 | env: 133 | DC: ${{ matrix.compiler.dmd }} 134 | run: | 135 | dub build 136 | dub test 137 | 138 | - uses: actions/upload-artifact@v4 139 | with: 140 | name: bin-${{matrix.build.type}}-${{matrix.build.version}}-${{ matrix.compiler.dmd }}-${{ matrix.host }} 141 | path: bin 142 | 143 | # Lint source code using the previously built binary 144 | - name: Run linter 145 | shell: bash 146 | env: 147 | REPORT_GITHUB: ${{matrix.do_report}} 148 | run: | 149 | if [ "$RUNNER_OS" == "Windows" ]; then 150 | EXE=".exe" 151 | else 152 | EXE="" 153 | fi 154 | if [ "$REPORT_GITHUB" == "1" ]; then 155 | FORMAT="github" 156 | else 157 | FORMAT="" 158 | fi 159 | "./bin/dscanner$EXE" --styleCheck -f "$FORMAT" src 160 | 161 | - name: Integration Tests 162 | run: ./it.sh 163 | working-directory: tests 164 | shell: bash 165 | 166 | # Parse phobos to check for failures / crashes / ... 167 | - name: Checkout Phobos 168 | uses: actions/checkout@v4 169 | with: 170 | repository: dlang/phobos 171 | path: phobos 172 | 173 | - name: Apply D-Scanner to Phobos 174 | if: ${{ matrix.build.version != 'min libdparse'}} # Older versions crash with "Invalid UTF..." 175 | working-directory: phobos 176 | shell: bash 177 | run: | 178 | for FILE in $(find std -name '*.d'); 179 | do 180 | echo "$FILE" 181 | ../bin/dscanner -S --config=.dscanner.ini "$FILE" 182 | done 183 | -------------------------------------------------------------------------------- /src/dscanner/analysis/unused_label.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.unused_label; 6 | 7 | import dscanner.analysis.base; 8 | import dscanner.analysis.helpers; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import dsymbol.scope_ : Scope; 12 | import std.algorithm.iteration : each; 13 | 14 | /** 15 | * Checks for labels that are never used. 16 | */ 17 | final class UnusedLabelCheck : BaseAnalyzer 18 | { 19 | alias visit = BaseAnalyzer.visit; 20 | 21 | mixin AnalyzerInfo!"unused_label_check"; 22 | 23 | /// 24 | this(BaseAnalyzerArguments args) 25 | { 26 | super(args); 27 | } 28 | 29 | override void visit(const Module mod) 30 | { 31 | pushScope(); 32 | mod.accept(this); 33 | popScope(); 34 | } 35 | 36 | override void visit(const FunctionLiteralExpression flit) 37 | { 38 | if (flit.specifiedFunctionBody) 39 | { 40 | pushScope(); 41 | flit.specifiedFunctionBody.accept(this); 42 | popScope(); 43 | } 44 | } 45 | 46 | override void visit(const FunctionBody functionBody) 47 | { 48 | if (functionBody.specifiedFunctionBody !is null) 49 | { 50 | pushScope(); 51 | functionBody.specifiedFunctionBody.accept(this); 52 | popScope(); 53 | } 54 | if (functionBody.missingFunctionBody && functionBody.missingFunctionBody.functionContracts) 55 | functionBody.missingFunctionBody.functionContracts.each!((a){pushScope(); a.accept(this); popScope();}); 56 | } 57 | 58 | override void visit(const LabeledStatement labeledStatement) 59 | { 60 | auto token = labeledStatement.identifier; 61 | Label* label = token.text in current; 62 | if (label is null) 63 | { 64 | current[token.text] = Label(token.text, token, false); 65 | } 66 | else 67 | { 68 | label.token = token; 69 | } 70 | if (labeledStatement.declarationOrStatement !is null) 71 | labeledStatement.declarationOrStatement.accept(this); 72 | } 73 | 74 | override void visit(const ContinueStatement contStatement) 75 | { 76 | if (contStatement.label.text.length) 77 | labelUsed(contStatement.label.text); 78 | } 79 | 80 | override void visit(const BreakStatement breakStatement) 81 | { 82 | if (breakStatement.label.text.length) 83 | labelUsed(breakStatement.label.text); 84 | } 85 | 86 | override void visit(const GotoStatement gotoStatement) 87 | { 88 | if (gotoStatement.label.text.length) 89 | labelUsed(gotoStatement.label.text); 90 | } 91 | 92 | override void visit(const AsmInstruction instr) 93 | { 94 | instr.accept(this); 95 | 96 | bool jmp; 97 | if (instr.identifierOrIntegerOrOpcode.text.length) 98 | jmp = instr.identifierOrIntegerOrOpcode.text[0] == 'j'; 99 | 100 | if (!jmp || !instr.operands || instr.operands.operands.length != 1) 101 | return; 102 | 103 | const AsmExp e = cast(AsmExp) instr.operands.operands[0]; 104 | if (e.left && cast(AsmBrExp) e.left) 105 | { 106 | const AsmBrExp b = cast(AsmBrExp) e.left; 107 | if (b && b.asmUnaExp && b.asmUnaExp.asmPrimaryExp) 108 | { 109 | const AsmPrimaryExp p = b.asmUnaExp.asmPrimaryExp; 110 | if (p && p.identifierChain && p.identifierChain.identifiers.length == 1) 111 | labelUsed(p.identifierChain.identifiers[0].text); 112 | } 113 | } 114 | } 115 | 116 | private: 117 | 118 | enum string KEY = "dscanner.suspicious.unused_label"; 119 | 120 | static struct Label 121 | { 122 | string name; 123 | Token token; 124 | bool used; 125 | } 126 | 127 | Label[string][] stack; 128 | 129 | auto ref current() 130 | { 131 | return stack[$ - 1]; 132 | } 133 | 134 | void pushScope() 135 | { 136 | stack.length++; 137 | } 138 | 139 | void popScope() 140 | { 141 | foreach (label; current.byValue()) 142 | { 143 | if (label.token is Token.init) 144 | { 145 | // TODO: handle unknown labels 146 | } 147 | else if (!label.used) 148 | { 149 | addErrorMessage(label.token, KEY, 150 | "Label \"" ~ label.name ~ "\" is not used."); 151 | } 152 | } 153 | stack.length--; 154 | } 155 | 156 | void labelUsed(string name) 157 | { 158 | Label* entry = name in current; 159 | if (entry is null) 160 | current[name] = Label(name, Token.init, true); 161 | else 162 | entry.used = true; 163 | } 164 | } 165 | 166 | unittest 167 | { 168 | import dscanner.analysis.config : Check, StaticAnalysisConfig, disabledConfig; 169 | import std.stdio : stderr; 170 | 171 | StaticAnalysisConfig sac = disabledConfig(); 172 | sac.unused_label_check = Check.enabled; 173 | assertAnalyzerWarnings(q{ 174 | int testUnusedLabel() 175 | { 176 | int x = 0; 177 | A: /+ 178 | ^ [warn]: Label "A" is not used. +/ 179 | if (x) goto B; 180 | x++; 181 | B: 182 | goto C; 183 | void foo() 184 | { 185 | C: /+ 186 | ^ [warn]: Label "C" is not used. +/ 187 | return; 188 | } 189 | C: 190 | void bar() 191 | { 192 | goto D; 193 | D: 194 | return; 195 | } 196 | D: /+ 197 | ^ [warn]: Label "D" is not used. +/ 198 | goto E; 199 | () { 200 | E: /+ 201 | ^ [warn]: Label "E" is not used. +/ 202 | return; 203 | }(); 204 | E: 205 | () { 206 | goto F; 207 | F: 208 | return; 209 | }(); 210 | F: /+ 211 | ^ [warn]: Label "F" is not used. +/ 212 | return x; 213 | G: /+ 214 | ^ [warn]: Label "G" is not used. +/ 215 | } 216 | }c, sac); 217 | 218 | assertAnalyzerWarnings(q{ 219 | void testAsm() 220 | { 221 | asm { jmp lbl;} 222 | lbl: 223 | } 224 | }c, sac); 225 | 226 | assertAnalyzerWarnings(q{ 227 | void testAsm() 228 | { 229 | asm { mov RAX,1;} 230 | lbl: /+ 231 | ^^^ [warn]: Label "lbl" is not used. +/ 232 | } 233 | }c, sac); 234 | 235 | // from std.math 236 | assertAnalyzerWarnings(q{ 237 | real polyImpl() { 238 | asm { 239 | jecxz return_ST; 240 | } 241 | } 242 | }c, sac); 243 | 244 | // a label might be hard to find, e.g. in a mixin 245 | assertAnalyzerWarnings(q{ 246 | real polyImpl() { 247 | mixin("return_ST: return 1;"); 248 | asm { 249 | jecxz return_ST; 250 | } 251 | } 252 | }c, sac); 253 | 254 | stderr.writeln("Unittest for UnusedLabelCheck passed."); 255 | } 256 | -------------------------------------------------------------------------------- /src/dscanner/analysis/label_var_same_name_check.d: -------------------------------------------------------------------------------- 1 | // Distributed under the Boost Software License, Version 1.0. 2 | // (See accompanying file LICENSE_1_0.txt or copy at 3 | // http://www.boost.org/LICENSE_1_0.txt) 4 | 5 | module dscanner.analysis.label_var_same_name_check; 6 | 7 | import dparse.ast; 8 | import dparse.lexer; 9 | import dsymbol.scope_ : Scope; 10 | import dscanner.analysis.base; 11 | import dscanner.analysis.helpers; 12 | 13 | /** 14 | * Checks for labels and variables that have the same name. 15 | */ 16 | final class LabelVarNameCheck : ScopedBaseAnalyzer 17 | { 18 | mixin AnalyzerInfo!"label_var_same_name_check"; 19 | 20 | this(BaseAnalyzerArguments args) 21 | { 22 | super(args); 23 | } 24 | 25 | mixin AggregateVisit!ClassDeclaration; 26 | mixin AggregateVisit!StructDeclaration; 27 | mixin AggregateVisit!InterfaceDeclaration; 28 | mixin AggregateVisit!UnionDeclaration; 29 | 30 | override void visit(const VariableDeclaration var) 31 | { 32 | foreach (dec; var.declarators) 33 | duplicateCheck(dec.name, false, conditionalDepth > 0); 34 | } 35 | 36 | override void visit(const LabeledStatement labeledStatement) 37 | { 38 | duplicateCheck(labeledStatement.identifier, true, conditionalDepth > 0); 39 | if (labeledStatement.declarationOrStatement !is null) 40 | labeledStatement.declarationOrStatement.accept(this); 41 | } 42 | 43 | override void visit(const ConditionalDeclaration condition) 44 | { 45 | if (condition.falseDeclarations.length > 0) 46 | ++conditionalDepth; 47 | condition.accept(this); 48 | if (condition.falseDeclarations.length > 0) 49 | --conditionalDepth; 50 | } 51 | 52 | override void visit(const VersionCondition condition) 53 | { 54 | ++conditionalDepth; 55 | condition.accept(this); 56 | --conditionalDepth; 57 | } 58 | 59 | alias visit = ScopedBaseAnalyzer.visit; 60 | 61 | private: 62 | 63 | enum string KEY = "dscanner.suspicious.label_var_same_name"; 64 | 65 | Thing[string][] stack; 66 | 67 | template AggregateVisit(NodeType) 68 | { 69 | override void visit(const NodeType n) 70 | { 71 | pushAggregateName(n.name); 72 | n.accept(this); 73 | popAggregateName(); 74 | } 75 | } 76 | 77 | void duplicateCheck(const Token name, bool fromLabel, bool isConditional) 78 | { 79 | import std.conv : to; 80 | import std.range : retro; 81 | 82 | size_t i; 83 | foreach (s; retro(stack)) 84 | { 85 | string fqn = parentAggregateText ~ name.text; 86 | const(Thing)* thing = fqn in s; 87 | if (thing is null) 88 | currentScope[fqn] = Thing(fqn, name.line, name.column, !fromLabel /+, isConditional+/ ); 89 | else if (i != 0 || !isConditional) 90 | { 91 | immutable thisKind = fromLabel ? "Label" : "Variable"; 92 | immutable otherKind = thing.isVar ? "variable" : "label"; 93 | addErrorMessage(name, KEY, 94 | thisKind ~ " \"" ~ fqn ~ "\" has the same name as a " 95 | ~ otherKind ~ " defined on line " ~ to!string(thing.line) ~ "."); 96 | } 97 | ++i; 98 | } 99 | } 100 | 101 | static struct Thing 102 | { 103 | string name; 104 | size_t line; 105 | size_t column; 106 | bool isVar; 107 | //bool isConditional; 108 | } 109 | 110 | ref currentScope() @property 111 | { 112 | return stack[$ - 1]; 113 | } 114 | 115 | protected override void pushScope() 116 | { 117 | stack.length++; 118 | } 119 | 120 | protected override void popScope() 121 | { 122 | stack.length--; 123 | } 124 | 125 | int conditionalDepth; 126 | 127 | void pushAggregateName(Token name) 128 | { 129 | parentAggregates ~= name; 130 | updateAggregateText(); 131 | } 132 | 133 | void popAggregateName() 134 | { 135 | parentAggregates.length -= 1; 136 | updateAggregateText(); 137 | } 138 | 139 | void updateAggregateText() 140 | { 141 | import std.algorithm : map; 142 | import std.array : join; 143 | 144 | if (parentAggregates.length) 145 | parentAggregateText = parentAggregates.map!(a => a.text).join(".") ~ "."; 146 | else 147 | parentAggregateText = ""; 148 | } 149 | 150 | Token[] parentAggregates; 151 | string parentAggregateText; 152 | } 153 | 154 | unittest 155 | { 156 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 157 | import std.stdio : stderr; 158 | 159 | StaticAnalysisConfig sac = disabledConfig(); 160 | sac.label_var_same_name_check = Check.enabled; 161 | assertAnalyzerWarnings(q{ 162 | unittest 163 | { 164 | blah: 165 | int blah; /+ 166 | ^^^^ [warn]: Variable "blah" has the same name as a label defined on line 4. +/ 167 | } 168 | int blah; 169 | unittest 170 | { 171 | static if (stuff) 172 | int a; 173 | int a; /+ 174 | ^ [warn]: Variable "a" has the same name as a variable defined on line 12. +/ 175 | } 176 | 177 | unittest 178 | { 179 | static if (stuff) 180 | int a = 10; 181 | else 182 | int a = 20; 183 | } 184 | 185 | unittest 186 | { 187 | static if (stuff) 188 | int a = 10; 189 | else 190 | int a = 20; 191 | int a; /+ 192 | ^ [warn]: Variable "a" has the same name as a variable defined on line 30. +/ 193 | } 194 | template T(stuff) 195 | { 196 | int b; 197 | } 198 | 199 | void main(string[] args) 200 | { 201 | for (int a = 0; a < 10; a++) 202 | things(a); 203 | 204 | for (int a = 0; a < 10; a++) 205 | things(a); 206 | int b; 207 | } 208 | 209 | unittest 210 | { 211 | version (Windows) 212 | int c = 10; 213 | else 214 | int c = 20; 215 | int c; /+ 216 | ^ [warn]: Variable "c" has the same name as a variable defined on line 54. +/ 217 | } 218 | 219 | unittest 220 | { 221 | version(LittleEndian) { enum string NAME = "UTF-16LE"; } 222 | else version(BigEndian) { enum string NAME = "UTF-16BE"; } 223 | } 224 | 225 | unittest 226 | { 227 | int a; 228 | struct A {int a;} 229 | } 230 | 231 | unittest 232 | { 233 | int a; 234 | struct A { struct A {int a;}} 235 | } 236 | 237 | unittest 238 | { 239 | int a; 240 | class A { class A {int a;}} 241 | } 242 | 243 | unittest 244 | { 245 | int a; 246 | interface A { interface A {int a;}} 247 | } 248 | 249 | unittest 250 | { 251 | interface A 252 | { 253 | int a; 254 | int a; /+ 255 | ^ [warn]: Variable "A.a" has the same name as a variable defined on line 93. +/ 256 | } 257 | } 258 | 259 | unittest 260 | { 261 | int aa; 262 | struct a { int a; } 263 | } 264 | 265 | unittest 266 | { 267 | switch (1) { 268 | case 1: 269 | int x, c1; 270 | break; 271 | case 2: 272 | int x, c2; 273 | break; 274 | default: 275 | int x, def; 276 | break; 277 | } 278 | } 279 | 280 | }c, sac); 281 | stderr.writeln("Unittest for LabelVarNameCheck passed."); 282 | } 283 | -------------------------------------------------------------------------------- /src/dscanner/analysis/body_on_disabled_funcs.d: -------------------------------------------------------------------------------- 1 | module dscanner.analysis.body_on_disabled_funcs; 2 | 3 | import dscanner.analysis.base; 4 | import dparse.ast; 5 | import dparse.lexer; 6 | import dsymbol.scope_; 7 | import std.meta : AliasSeq; 8 | 9 | final class BodyOnDisabledFuncsCheck : BaseAnalyzer 10 | { 11 | alias visit = BaseAnalyzer.visit; 12 | 13 | mixin AnalyzerInfo!"body_on_disabled_func_check"; 14 | 15 | this(BaseAnalyzerArguments args) 16 | { 17 | super(args); 18 | } 19 | 20 | static foreach (AggregateType; AliasSeq!(InterfaceDeclaration, ClassDeclaration, 21 | StructDeclaration, UnionDeclaration, FunctionDeclaration)) 22 | override void visit(const AggregateType t) 23 | { 24 | scope wasDisabled = isDisabled; 25 | isDisabled = false; 26 | t.accept(this); 27 | isDisabled = wasDisabled; 28 | } 29 | 30 | override void visit(const Declaration dec) 31 | { 32 | foreach (attr; dec.attributes) 33 | { 34 | if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable") { 35 | // found attr block w. disable: dec.constructor 36 | scope wasDisabled = isDisabled; 37 | isDisabled = true; 38 | visitDeclarationInner(dec); 39 | dec.accept(this); 40 | isDisabled = wasDisabled; 41 | return; 42 | } 43 | } 44 | 45 | visitDeclarationInner(dec); 46 | scope wasDisabled = isDisabled; 47 | dec.accept(this); 48 | isDisabled = wasDisabled; 49 | } 50 | 51 | private: 52 | bool isDisabled = false; 53 | 54 | bool isDeclDisabled(T)(const T dec) 55 | { 56 | // `@disable { ... }` 57 | if (isDisabled) 58 | return true; 59 | 60 | static if (__traits(hasMember, T, "storageClasses")) 61 | { 62 | // `@disable doThing() {}` 63 | if (hasDisabledStorageclass(dec.storageClasses)) 64 | return true; 65 | } 66 | 67 | // `void doThing() @disable {}` 68 | return hasDisabledMemberFunctionAttribute(dec.memberFunctionAttributes); 69 | } 70 | 71 | void visitDeclarationInner(const Declaration dec) 72 | { 73 | if (dec.attributeDeclaration !is null 74 | && dec.attributeDeclaration.attribute 75 | && dec.attributeDeclaration.attribute.atAttribute 76 | && dec.attributeDeclaration.attribute.atAttribute.identifier.text == "disable") 77 | { 78 | // found `@disable:`, so all code in this block is now disabled 79 | isDisabled = true; 80 | } 81 | else if (dec.functionDeclaration !is null 82 | && isDeclDisabled(dec.functionDeclaration) 83 | && dec.functionDeclaration.functionBody !is null 84 | && dec.functionDeclaration.functionBody.missingFunctionBody is null) 85 | { 86 | addErrorMessage(dec.functionDeclaration.functionBody, 87 | KEY, "Function marked with '@disabled' should not have a body"); 88 | } 89 | else if (dec.constructor !is null 90 | && isDeclDisabled(dec.constructor) 91 | && dec.constructor.functionBody !is null 92 | && dec.constructor.functionBody.missingFunctionBody is null) 93 | { 94 | addErrorMessage(dec.constructor.functionBody, 95 | KEY, "Constructor marked with '@disabled' should not have a body"); 96 | } 97 | else if (dec.destructor !is null 98 | && isDeclDisabled(dec.destructor) 99 | && dec.destructor.functionBody !is null 100 | && dec.destructor.functionBody.missingFunctionBody is null) 101 | { 102 | addErrorMessage(dec.destructor.functionBody, 103 | KEY, "Destructor marked with '@disabled' should not have a body"); 104 | } 105 | } 106 | 107 | bool hasDisabledStorageclass(const(StorageClass[]) storageClasses) 108 | { 109 | foreach (sc; storageClasses) 110 | { 111 | if (sc.atAttribute !is null && sc.atAttribute.identifier.text == "disable") 112 | return true; 113 | } 114 | return false; 115 | } 116 | 117 | bool hasDisabledMemberFunctionAttribute(const(MemberFunctionAttribute[]) memberFunctionAttributes) 118 | { 119 | foreach (attr; memberFunctionAttributes) 120 | { 121 | if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable") 122 | return true; 123 | } 124 | return false; 125 | } 126 | 127 | enum string KEY = "dscanner.confusing.disabled_function_with_body"; 128 | } 129 | 130 | unittest 131 | { 132 | import std.stdio : stderr; 133 | import std.format : format; 134 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 135 | import dscanner.analysis.helpers : assertAnalyzerWarnings; 136 | 137 | StaticAnalysisConfig sac = disabledConfig(); 138 | sac.body_on_disabled_func_check = Check.enabled; 139 | 140 | assertAnalyzerWarnings(q{ 141 | class C1 142 | { 143 | this() {} 144 | void doThing() {} 145 | ~this() {} 146 | 147 | this(); 148 | void doThing(); 149 | ~this(); 150 | 151 | @disable: 152 | @disable 153 | { 154 | class UnaffectedSubClass 155 | { 156 | this() {} 157 | void doThing() {} 158 | ~this() {} 159 | } 160 | } 161 | 162 | this() {} /+ 163 | ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ 164 | void doThing() {} /+ 165 | ^^ [warn]: Function marked with '@disabled' should not have a body +/ 166 | ~this() {} /+ 167 | ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ 168 | 169 | this(); 170 | void doThing(); 171 | ~this(); 172 | } 173 | 174 | class C2 175 | { 176 | @disable this() {} /+ 177 | ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ 178 | @disable { this() {} } /+ 179 | ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ 180 | this() @disable {} /+ 181 | ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ 182 | 183 | @disable void doThing() {} /+ 184 | ^^ [warn]: Function marked with '@disabled' should not have a body +/ 185 | @disable doThing() {} /+ 186 | ^^ [warn]: Function marked with '@disabled' should not have a body +/ 187 | @disable { void doThing() {} } /+ 188 | ^^ [warn]: Function marked with '@disabled' should not have a body +/ 189 | void doThing() @disable {} /+ 190 | ^^ [warn]: Function marked with '@disabled' should not have a body +/ 191 | 192 | @disable ~this() {} /+ 193 | ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ 194 | @disable { ~this() {} } /+ 195 | ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ 196 | ~this() @disable {} /+ 197 | ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ 198 | 199 | @disable this(); 200 | @disable { this(); } 201 | this() @disable; 202 | 203 | @disable void doThing(); 204 | // @disable doThing(); // this is invalid grammar! 205 | @disable { void doThing(); } 206 | void doThing() @disable; 207 | 208 | @disable ~this(); 209 | @disable { ~this(); } 210 | ~this() @disable; 211 | } 212 | }c, sac); 213 | 214 | stderr.writeln("Unittest for BodyOnDisabledFuncsCheck passed."); 215 | } 216 | -------------------------------------------------------------------------------- /src/dscanner/analysis/style.d: -------------------------------------------------------------------------------- 1 | // Copyright Brian Schott (Hackerpilot) 2014. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // http://www.boost.org/LICENSE_1_0.txt) 5 | 6 | module dscanner.analysis.style; 7 | 8 | import std.stdio; 9 | import dparse.ast; 10 | import dparse.lexer; 11 | import std.regex; 12 | import std.array; 13 | import std.conv; 14 | import std.format; 15 | import dscanner.analysis.helpers; 16 | import dscanner.analysis.base; 17 | import dscanner.analysis.nolint; 18 | import dsymbol.scope_ : Scope; 19 | 20 | final class StyleChecker : BaseAnalyzer 21 | { 22 | alias visit = ASTVisitor.visit; 23 | 24 | enum string varFunNameRegex = `^([\p{Ll}_][_\w\d]*|[\p{Lu}\d_]+)$`; 25 | enum string aggregateNameRegex = `^\p{Lu}[\w\d]*$`; 26 | enum string moduleNameRegex = `^[\p{Ll}_\d]+$`; 27 | enum string KEY = "dscanner.style.phobos_naming_convention"; 28 | mixin AnalyzerInfo!"style_check"; 29 | 30 | this(BaseAnalyzerArguments args) 31 | { 32 | super(args); 33 | } 34 | 35 | override void visit(const ModuleDeclaration dec) 36 | { 37 | with (noLint.push(NoLintFactory.fromModuleDeclaration(dec))) 38 | dec.accept(this); 39 | 40 | foreach (part; dec.moduleName.identifiers) 41 | { 42 | if (part.text.matchFirst(moduleNameRegex).length == 0) 43 | addErrorMessage(part, KEY, 44 | "Module/package name '" ~ part.text ~ "' does not match style guidelines."); 45 | } 46 | } 47 | 48 | // "extern (Windows) {}" : push visit pop 49 | override void visit(const Declaration dec) 50 | { 51 | bool p; 52 | if (dec.attributes) 53 | foreach (attrib; dec.attributes) 54 | if (const LinkageAttribute la = attrib.linkageAttribute) 55 | { 56 | p = true; 57 | pushWinStyle(la.identifier.text.length && la.identifier.text == "Windows"); 58 | } 59 | 60 | dec.accept(this); 61 | 62 | if (p) 63 | popWinStyle; 64 | } 65 | 66 | // "extern (Windows) :" : overwrite current 67 | override void visit(const AttributeDeclaration dec) 68 | { 69 | if (dec.attribute && dec.attribute.linkageAttribute) 70 | { 71 | const LinkageAttribute la = dec.attribute.linkageAttribute; 72 | _winStyles[$-1] = la.identifier.text.length && la.identifier.text == "Windows"; 73 | } 74 | } 75 | 76 | override void visit(const VariableDeclaration vd) 77 | { 78 | vd.accept(this); 79 | } 80 | 81 | override void visit(const Declarator dec) 82 | { 83 | checkLowercaseName("Variable", dec.name); 84 | } 85 | 86 | override void visit(const FunctionDeclaration dec) 87 | { 88 | // "extern(Windows) Name();" push visit pop 89 | bool p; 90 | if (dec.attributes) 91 | foreach (attrib; dec.attributes) 92 | if (const LinkageAttribute la = attrib.linkageAttribute) 93 | { 94 | p = true; 95 | pushWinStyle(la.identifier.text.length && la.identifier.text == "Windows"); 96 | } 97 | 98 | if (dec.functionBody.specifiedFunctionBody || 99 | (dec.functionBody.missingFunctionBody && !winStyle())) 100 | checkLowercaseName("Function", dec.name); 101 | 102 | if (p) 103 | popWinStyle; 104 | } 105 | 106 | void checkLowercaseName(string type, ref const Token name) 107 | { 108 | if (name.text.length > 0 && name.text.matchFirst(varFunNameRegex).length == 0) 109 | addErrorMessage(name, KEY, 110 | type ~ " name '" ~ name.text ~ "' does not match style guidelines."); 111 | } 112 | 113 | override void visit(const ClassDeclaration dec) 114 | { 115 | checkAggregateName("Class", dec.name); 116 | dec.accept(this); 117 | } 118 | 119 | override void visit(const InterfaceDeclaration dec) 120 | { 121 | checkAggregateName("Interface", dec.name); 122 | dec.accept(this); 123 | } 124 | 125 | override void visit(const EnumDeclaration dec) 126 | { 127 | if (dec.name.text is null || dec.name.text.length == 0) 128 | return; 129 | checkAggregateName("Enum", dec.name); 130 | dec.accept(this); 131 | } 132 | 133 | override void visit(const StructDeclaration dec) 134 | { 135 | checkAggregateName("Struct", dec.name); 136 | dec.accept(this); 137 | } 138 | 139 | void checkAggregateName(string aggregateType, ref const Token name) 140 | { 141 | if (name.text.length > 0 && name.text.matchFirst(aggregateNameRegex).length == 0) 142 | addErrorMessage(name, KEY, 143 | aggregateType ~ " name '" ~ name.text ~ "' does not match style guidelines."); 144 | } 145 | 146 | bool[] _winStyles = [false]; 147 | 148 | bool winStyle() 149 | { 150 | return _winStyles[$-1]; 151 | } 152 | 153 | void pushWinStyle(const bool value) 154 | { 155 | _winStyles.length += 1; 156 | _winStyles[$-1] = value; 157 | } 158 | 159 | void popWinStyle() 160 | { 161 | _winStyles.length -= 1; 162 | } 163 | } 164 | 165 | unittest 166 | { 167 | import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 168 | 169 | StaticAnalysisConfig sac = disabledConfig(); 170 | sac.style_check = Check.enabled; 171 | 172 | assertAnalyzerWarnings(q{ 173 | module AMODULE; /+ 174 | ^^^^^^^ [warn]: Module/package name 'AMODULE' does not match style guidelines. +/ 175 | 176 | bool A_VARIABLE; // FIXME: 177 | bool a_variable; // ok 178 | bool aVariable; // ok 179 | 180 | void A_FUNCTION() {} // FIXME: 181 | class cat {} /+ 182 | ^^^ [warn]: Class name 'cat' does not match style guidelines. +/ 183 | interface puma {} /+ 184 | ^^^^ [warn]: Interface name 'puma' does not match style guidelines. +/ 185 | struct dog {} /+ 186 | ^^^ [warn]: Struct name 'dog' does not match style guidelines. +/ 187 | enum racoon { a } /+ 188 | ^^^^^^ [warn]: Enum name 'racoon' does not match style guidelines. +/ 189 | enum bool something = false; 190 | enum bool someThing = false; 191 | enum Cat { fritz, } 192 | enum Cat = Cat.fritz; 193 | }c, sac); 194 | 195 | assertAnalyzerWarnings(q{ 196 | extern(Windows) 197 | { 198 | bool Fun0(); 199 | extern(Windows) bool Fun1(); 200 | } 201 | }c, sac); 202 | 203 | assertAnalyzerWarnings(q{ 204 | extern(Windows) 205 | { 206 | extern(D) bool Fun2(); /+ 207 | ^^^^ [warn]: Function name 'Fun2' does not match style guidelines. +/ 208 | bool Fun3(); 209 | } 210 | }c, sac); 211 | 212 | assertAnalyzerWarnings(q{ 213 | extern(Windows) 214 | { 215 | extern(C): 216 | extern(D) bool Fun4(); /+ 217 | ^^^^ [warn]: Function name 'Fun4' does not match style guidelines. +/ 218 | bool Fun5(); /+ 219 | ^^^^ [warn]: Function name 'Fun5' does not match style guidelines. +/ 220 | } 221 | }c, sac); 222 | 223 | assertAnalyzerWarnings(q{ 224 | extern(Windows): 225 | bool Fun6(); 226 | bool Fun7(); 227 | extern(D): 228 | void okOkay(); 229 | void NotReallyOkay(); /+ 230 | ^^^^^^^^^^^^^ [warn]: Function name 'NotReallyOkay' does not match style guidelines. +/ 231 | }c, sac); 232 | 233 | assertAnalyzerWarnings(q{ 234 | extern(Windows): 235 | bool WinButWithBody(){} /+ 236 | ^^^^^^^^^^^^^^ [warn]: Function name 'WinButWithBody' does not match style guidelines. +/ 237 | }c, sac); 238 | 239 | stderr.writeln("Unittest for StyleChecker passed."); 240 | } 241 | -------------------------------------------------------------------------------- /src/dscanner/analysis/nolint.d: -------------------------------------------------------------------------------- 1 | module dscanner.analysis.nolint; 2 | 3 | @safe: 4 | 5 | import dparse.ast; 6 | import dparse.lexer; 7 | 8 | import std.algorithm : canFind; 9 | import std.regex : matchAll, regex; 10 | import std.string : lastIndexOf, strip; 11 | import std.typecons; 12 | 13 | struct NoLint 14 | { 15 | bool containsCheck(scope const(char)[] check) const 16 | { 17 | while (true) 18 | { 19 | if (disabledChecks.get((() @trusted => cast(string) check)(), 0) > 0) 20 | return true; 21 | 22 | auto dot = check.lastIndexOf('.'); 23 | if (dot == -1) 24 | break; 25 | check = check[0 .. dot]; 26 | } 27 | return false; 28 | } 29 | 30 | // automatic pop when returned value goes out of scope 31 | Poppable push(in Nullable!NoLint other) scope 32 | { 33 | if (other.isNull) 34 | return Poppable(null); 35 | 36 | foreach (key, value; other.get.getDisabledChecks) 37 | this.disabledChecks[key] += value; 38 | 39 | return Poppable(() => this.pop(other)); 40 | } 41 | 42 | package: 43 | const(int[string]) getDisabledChecks() const 44 | { 45 | return this.disabledChecks; 46 | } 47 | 48 | void pushCheck(in string check) 49 | { 50 | disabledChecks[check]++; 51 | } 52 | 53 | void merge(in Nullable!NoLint other) 54 | { 55 | if (other.isNull) 56 | return; 57 | 58 | foreach (key, value; other.get.getDisabledChecks) 59 | this.disabledChecks[key] += value; 60 | } 61 | 62 | private: 63 | void pop(in Nullable!NoLint other) 64 | { 65 | if (other.isNull) 66 | return; 67 | 68 | foreach (key, value; other.get.getDisabledChecks) 69 | { 70 | assert(this.disabledChecks.get(key, 0) >= value); 71 | 72 | this.disabledChecks[key] -= value; 73 | } 74 | } 75 | 76 | static struct Poppable 77 | { 78 | ~this() 79 | { 80 | if (onPop) 81 | onPop(); 82 | onPop = null; 83 | } 84 | 85 | private: 86 | void delegate() onPop; 87 | } 88 | 89 | int[string] disabledChecks; 90 | } 91 | 92 | struct NoLintFactory 93 | { 94 | static Nullable!NoLint fromModuleDeclaration(in ModuleDeclaration moduleDeclaration) 95 | { 96 | NoLint noLint; 97 | 98 | foreach (atAttribute; moduleDeclaration.atAttributes) 99 | noLint.merge(NoLintFactory.fromAtAttribute(atAttribute)); 100 | 101 | if (!noLint.getDisabledChecks.length) 102 | return nullNoLint; 103 | 104 | return noLint.nullable; 105 | } 106 | 107 | static Nullable!NoLint fromDeclaration(in Declaration declaration) 108 | { 109 | NoLint noLint; 110 | foreach (attribute; declaration.attributes) 111 | noLint.merge(NoLintFactory.fromAttribute(attribute)); 112 | 113 | if (!noLint.getDisabledChecks.length) 114 | return nullNoLint; 115 | 116 | return noLint.nullable; 117 | } 118 | 119 | private: 120 | static Nullable!NoLint fromAttribute(const(Attribute) attribute) 121 | { 122 | if (attribute is null) 123 | return nullNoLint; 124 | 125 | return NoLintFactory.fromAtAttribute(attribute.atAttribute); 126 | 127 | } 128 | 129 | static Nullable!NoLint fromAtAttribute(const(AtAttribute) atAttribute) 130 | { 131 | if (atAttribute is null) 132 | return nullNoLint; 133 | 134 | auto ident = atAttribute.identifier; 135 | auto argumentList = atAttribute.argumentList; 136 | 137 | if (argumentList !is null) 138 | { 139 | if (ident.text.length) 140 | return NoLintFactory.fromStructUda(ident, argumentList); 141 | else 142 | return NoLintFactory.fromStringUda(argumentList); 143 | 144 | } 145 | else 146 | return nullNoLint; 147 | } 148 | 149 | // @nolint("..") 150 | static Nullable!NoLint fromStructUda(in Token ident, in ArgumentList argumentList) 151 | in (ident.text.length && argumentList !is null) 152 | { 153 | if (ident.text != "nolint") 154 | return nullNoLint; 155 | 156 | NoLint noLint; 157 | 158 | foreach (nodeExpr; argumentList.items) 159 | { 160 | if (auto unaryExpr = cast(const UnaryExpression) nodeExpr) 161 | { 162 | auto primaryExpression = unaryExpr.primaryExpression; 163 | if (primaryExpression is null) 164 | continue; 165 | 166 | if (primaryExpression.primary != tok!"stringLiteral") 167 | continue; 168 | 169 | noLint.pushCheck(primaryExpression.primary.text.strip("\"")); 170 | } 171 | } 172 | 173 | if (!noLint.getDisabledChecks().length) 174 | return nullNoLint; 175 | 176 | return noLint.nullable; 177 | } 178 | 179 | // @("nolint(..)") 180 | static Nullable!NoLint fromStringUda(in ArgumentList argumentList) 181 | in (argumentList !is null) 182 | { 183 | NoLint noLint; 184 | 185 | foreach (nodeExpr; argumentList.items) 186 | { 187 | if (auto unaryExpr = cast(const UnaryExpression) nodeExpr) 188 | { 189 | auto primaryExpression = unaryExpr.primaryExpression; 190 | if (primaryExpression is null) 191 | continue; 192 | 193 | if (primaryExpression.primary != tok!"stringLiteral") 194 | continue; 195 | 196 | auto str = primaryExpression.primary.text.strip("\""); 197 | Nullable!NoLint currNoLint = NoLintFactory.fromString(str); 198 | noLint.merge(currNoLint); 199 | } 200 | } 201 | 202 | if (!noLint.getDisabledChecks().length) 203 | return nullNoLint; 204 | 205 | return noLint.nullable; 206 | 207 | } 208 | 209 | // Transform a string with form "nolint(abc, efg)" 210 | // into a NoLint struct 211 | static Nullable!NoLint fromString(in string str) 212 | { 213 | static immutable re = regex(`[\w-_.]+`, "g"); 214 | auto matches = matchAll(str, re); 215 | 216 | if (!matches) 217 | return nullNoLint; 218 | 219 | const udaName = matches.hit; 220 | if (udaName != "nolint") 221 | return nullNoLint; 222 | 223 | matches.popFront; 224 | 225 | NoLint noLint; 226 | 227 | while (matches) 228 | { 229 | noLint.pushCheck(matches.hit); 230 | matches.popFront; 231 | } 232 | 233 | if (!noLint.getDisabledChecks.length) 234 | return nullNoLint; 235 | 236 | return noLint.nullable; 237 | } 238 | 239 | static nullNoLint = Nullable!NoLint.init; 240 | } 241 | 242 | unittest 243 | { 244 | const s1 = "nolint(abc)"; 245 | const s2 = "nolint(abc, efg, hij)"; 246 | const s3 = " nolint ( abc , efg ) "; 247 | const s4 = "nolint(dscanner.style.abc_efg-ijh)"; 248 | const s5 = "OtherUda(abc)"; 249 | const s6 = "nolint(dscanner)"; 250 | 251 | assert(NoLintFactory.fromString(s1).get.containsCheck("abc")); 252 | 253 | assert(NoLintFactory.fromString(s2).get.containsCheck("abc")); 254 | assert(NoLintFactory.fromString(s2).get.containsCheck("efg")); 255 | assert(NoLintFactory.fromString(s2).get.containsCheck("hij")); 256 | 257 | assert(NoLintFactory.fromString(s3).get.containsCheck("abc")); 258 | assert(NoLintFactory.fromString(s3).get.containsCheck("efg")); 259 | 260 | assert(NoLintFactory.fromString(s4).get.containsCheck("dscanner.style.abc_efg-ijh")); 261 | 262 | assert(NoLintFactory.fromString(s5).isNull); 263 | 264 | assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner")); 265 | assert(!NoLintFactory.fromString(s6).get.containsCheck("dscanner2")); 266 | assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner.foo")); 267 | 268 | import std.stdio : stderr, writeln; 269 | 270 | (() @trusted => stderr.writeln("Unittest for NoLint passed."))(); 271 | } 272 | --------------------------------------------------------------------------------