diff --git a/.editorconfig b/.editorconfig index 96fa7b7e..23b565f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,4 +22,4 @@ dfmt_template_constraint_style = conditional_newline_indent [*.yml] indent_style = space -indent_size = 2 +indent_size = 2 \ No newline at end of file diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 11a1f2ac..f0fc196a 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -87,6 +87,10 @@ jobs: submodules: 'recursive' fetch-depth: 0 + # Uncomment to get a ssh connection inside the GH Actions runner + - name: Setup upterm session + uses: lhotari/action-upterm@v1 + # Install the host compiler (DMD or LDC) # Also grabs DMD for GDC to include dub + rdmd - name: Install ${{ matrix.compiler.version }} @@ -102,6 +106,10 @@ jobs: sudo apt-get install gdc-12 -y gdc-12 --version + - name: Setup upterm session + if: ${{ matrix.build.type == 'dub' && matrix.host == 'ubuntu-22.04'}} + uses: lhotari/action-upterm@v1 + # Compile D-Scanner and execute all tests without dub - name: Build and test without dub if: ${{ matrix.build.type == 'make' }} @@ -114,7 +122,8 @@ jobs: ./build.bat ./build.bat test else - make "-j$(nproc)" all test + NUM_PROC=$(nproc || getconf _NPROCESSORS_ONLN || 1) + make "-j$((NUM_PROC / 2))" all test fi # Compile D-Scanner and execute all tests using a specific dependency version @@ -135,7 +144,7 @@ jobs: dub build dub test - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: bin-${{matrix.build.type}}-${{matrix.build.version}}-${{ matrix.compiler.dmd }}-${{ matrix.host }} path: bin @@ -159,10 +168,20 @@ jobs: "./bin/dscanner$EXE" --styleCheck -f "$FORMAT" src - name: Integration Tests - run: ./it.sh + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + ./it.sh Windows + else + ./it.sh Unix + fi working-directory: tests shell: bash + - name: Run style checks + if: ${{ matrix.compiler.dmd == 'dmd' && matrix.build.type == 'make' }} + run: | + make style + # Parse phobos to check for failures / crashes / ... - name: Checkout Phobos uses: actions/checkout@v4 @@ -171,7 +190,7 @@ jobs: path: phobos - name: Apply D-Scanner to Phobos - if: ${{ matrix.build.version != 'min libdparse'}} # Older versions crash with "Invalid UTF..." + if: ${{ matrix.build.version != 'min libdparse'}} # Older versions crash with "Invalid UTF..." working-directory: phobos shell: bash run: | diff --git a/.gitignore b/.gitignore index 4d65886e..6e1d6c59 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Sublime Text 2 *.sublime-workspace +# Idea stuff +.idea/ + # Subversion .svn/ diff --git a/.gitmodules b/.gitmodules index 12d4fee0..52ede99b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ [submodule "DCD"] path = DCD url = https://github.com/dlang-community/DCD.git +[submodule "dmd"] + path = dmd + url = git@github.com:dlang/dmd.git diff --git a/README.md b/README.md index 33ca911c..72c71f3e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,15 @@ D-Scanner is a tool for analyzing D source code ### Building and installing -First make sure that you have all the source code. Run ```git submodule update --init --recursive``` + +First, make sure that you have fetched the upstream: git@github.com:dlang-community/D-Scanner.git + +``` +git remote add upstream git@github.com:dlang-community/D-Scanner.git +git fetch upstream +``` + +Secondly, make sure that you have all the source code. Run ```git submodule update --init --recursive``` after cloning the project. To build D-Scanner, run ```make``` (or the build.bat file on Windows). diff --git a/build.bat b/build.bat index ad3c35cc..0a565a35 100644 --- a/build.bat +++ b/build.bat @@ -18,8 +18,8 @@ if %githashsize% == 0 ( move /y bin\githash_.txt bin\githash.txt ) -set DFLAGS=-O -release -Jbin %MFLAGS% -set TESTFLAGS=-g -w -Jbin +set DFLAGS=-O -release -version=StdLoggerDisableWarning -version=CallbackAPI -version=DMDLIB -version=MARS -version=NoBackend -version=NoMain -Jbin -Jdmd -Jdmd\compiler\src\dmd\res %MFLAGS% +set TESTFLAGS=-g -w -version=StdLoggerDisableWarning -version=CallbackAPI -version=DMDLIB -version=MARS -version=NoBackend -version=NoMain -Jbin -Jdmd -Jdmd\compiler\src\dmd\res set CORE= set LIBDPARSE= set STD= @@ -29,6 +29,44 @@ set DSYMBOL= set CONTAINERS= set LIBDDOC= +set DMD_FRONTEND_DENYLIST=^ + dmd\compiler\src\dmd\mars.d^ + dmd\compiler\src\dmd\dmsc.d^ + dmd\compiler\src\dmd\e2ir.d^ + dmd\compiler\src\dmd\eh.d^ + dmd\compiler\src\dmd\glue.d^ + dmd\compiler\src\dmd\iasmdmd.d^ + dmd\compiler\src\dmd\irstate.d^ + dmd\compiler\src\dmd\lib.d^ + dmd\compiler\src\dmd\libelf.d^ + dmd\compiler\src\dmd\libmach.d^ + dmd\compiler\src\dmd\libmscoff.d^ + dmd\compiler\src\dmd\libomf.d^ + dmd\compiler\src\dmd\objc_glue.d^ + dmd\compiler\src\dmd\s2ir.d^ + dmd\compiler\src\dmd\scanelf.d^ + dmd\compiler\src\dmd\scanmach.d^ + dmd\compiler\src\dmd\scanmscoff.d^ + dmd\compiler\src\dmd\scanomf.d^ + dmd\compiler\src\dmd\tocsym.d^ + dmd\compiler\src\dmd\toctype.d^ + dmd\compiler\src\dmd\tocvdebug.d^ + dmd\compiler\src\dmd\toobj.d^ + dmd\compiler\src\dmd\todt.d^ + dmd\compiler\src\dmd\toir.d + +set DMD_FRONTEND_SRC= +for %%x in (dmd\compiler\src\dmd\common\*.d) do set DMD_FRONTEND_SRC=!DMD_FRONTEND_SRC! %%x +for %%x in (dmd\compiler\src\dmd\root\*.d) do set DMD_FRONTEND_SRC=!DMD_FRONTEND_SRC! %%x +for %%x in (dmd\compiler\src\dmd\visitor\*.d) do set DMD_FRONTEND_SRC=!DMD_FRONTEND_SRC! %%x +for %%x in (dmd\compiler\src\dmd\mangle\*.d) do set DMD_FRONTEND_SRC=!DMD_FRONTEND_SRC! %%x +for %%x in (dmd\compiler\src\dmd\*.d) do ( + echo "%DMD_FRONTEND_DENYLIST%" | findstr /i /c:"%%x" >nul + if errorlevel 1 ( + set "DMD_FRONTEND_SRC=!DMD_FRONTEND_SRC! %%x" + ) +) + for %%x in (src\dscanner\*.d) do set CORE=!CORE! %%x for %%x in (src\dscanner\analysis\*.d) do set ANALYSIS=!ANALYSIS! %%x for %%x in (libdparse\src\dparse\*.d) do set LIBDPARSE=!LIBDPARSE! %%x @@ -45,14 +83,62 @@ for %%x in (containers\src\containers\internal\*.d) do set CONTAINERS=!CONTAINER if "%1" == "test" goto test_cmd @echo on -%DC% %MFLAGS% %CORE% %STD% %LIBDPARSE% %LIBDDOC% %ANALYSIS% %INIFILED% %DSYMBOL% %CONTAINERS% %DFLAGS% -I"libdparse\src" -I"DCD\dsymbol\src" -I"containers\src" -I"libddoc\src" -I"libddoc\common\source" -ofbin\dscanner.exe +%DC% %MFLAGS%^ + %CORE%^ + %STD%^ + %LIBDPARSE%^ + %LIBDDOC%^ + %ANALYSIS%^ + %INIFILED%^ + %DSYMBOL%^ + %CONTAINERS%^ + %DMD_FRONTEND_SRC%^ + %DFLAGS%^ + -Ilibdparse\src^ + -IDCD\dsymbol\src^ + -Icontainers\src^ + -Ilibddoc\src^ + -Ilibddoc\common\source^ + -Idmd\compiler\src^ + -ofbin\dscanner.exe goto eof :test_cmd @echo on set TESTNAME="bin\dscanner-unittest" -%DC% %MFLAGS% %STD% %LIBDPARSE% %LIBDDOC% %INIFILED% %DSYMBOL% %CONTAINERS% -I"libdparse\src" -I"DCD\dsymbol\src" -I"containers\src" -I"libddoc\src" -lib %TESTFLAGS% -of%TESTNAME%.lib -if exist %TESTNAME%.lib %DC% %MFLAGS% %CORE% %ANALYSIS% %TESTNAME%.lib -I"src" -I"inifiled\source" -I"libdparse\src" -I"DCD\dsymbol\src" -I"containers\src" -I"libddoc\src" -I"libddoc\common\source" -unittest %TESTFLAGS% -of%TESTNAME%.exe +%DC% %MFLAGS% ^ + %STD%^ + %LIBDPARSE%^ + %LIBDDOC%^ + %INIFILED%^ + %DSYMBOL%^ + %CONTAINERS%^ + %DMD_FRONTEND_SRC%^ + -I"libdparse\src"^ + -I"DCD\dsymbol\src"^ + -I"containers\src"^ + -I"libddoc\src"^ + -I"dmd\compiler\src"^ + -I"dmd\compiler\src\dmd\res"^ + %TESTFLAGS%^ + -lib^ + -of%TESTNAME%.lib +if exist %TESTNAME%.lib %DC% %MFLAGS%^ + %CORE%^ + %ANALYSIS%^ + %TESTNAME%.lib^ + -I"src"^ + -I"inifiled\source"^ + -I"libdparse\src"^ + -I"DCD\dsymbol\src"^ + -I"containers\src"^ + -I"libddoc\src"^ + -I"libddoc\common\source"^ + -I"dmd\compiler\src"^ + -I"dmd\compiler\src\dmd\res"^ + -unittest^ + %TESTFLAGS%^ + -of%TESTNAME%.exe if exist %TESTNAME%.exe %TESTNAME%.exe if exist %TESTNAME%.obj del %TESTNAME%.obj diff --git a/changelog/dscanner.assert-without-message.dd b/changelog/dscanner.assert-without-message.dd new file mode 100644 index 00000000..2ea35b66 --- /dev/null +++ b/changelog/dscanner.assert-without-message.dd @@ -0,0 +1 @@ +Avoid checking `enforce` calls as it is phobos specific. \ No newline at end of file diff --git a/changelog/dscanner.comma-expression.dd b/changelog/dscanner.comma-expression.dd new file mode 100644 index 00000000..51833a36 --- /dev/null +++ b/changelog/dscanner.comma-expression.dd @@ -0,0 +1,3 @@ +Remove the check for comma expression check + e.g. (int a = 3, a + 7) +This check is no longer necessary since comma expression have been removed from the D language. diff --git a/changelog/dscanner.duplicate-attribute-check.dd b/changelog/dscanner.duplicate-attribute-check.dd new file mode 100644 index 00000000..d2d045d0 --- /dev/null +++ b/changelog/dscanner.duplicate-attribute-check.dd @@ -0,0 +1,2 @@ +Remove the check for duplicate attributes (@property, @safe, @trusted, @system, pure, nothrow). +This check is no longer necessary since having duplicated attributes is now a compiler error. diff --git a/changelog/dscanner.if-statements.dd b/changelog/dscanner.if-statements.dd new file mode 100644 index 00000000..15e8a4aa --- /dev/null +++ b/changelog/dscanner.if-statements.dd @@ -0,0 +1 @@ +Remove IfStatementCheck, as it has been disabled in 2015 due to false positives and untouched ever since then. diff --git a/changelog/dscanner.struct-ctor-check.dd b/changelog/dscanner.struct-ctor-check.dd new file mode 100644 index 00000000..27940c86 --- /dev/null +++ b/changelog/dscanner.struct-ctor-check.dd @@ -0,0 +1,19 @@ +Remove the check regarding structs with no arguments constructors. + +The check is implemented in constructors.d and it warns against the usage +of both constructors with all parameters with default values and constructors +without any arguments, as this might be confusing. This scenario, for structs, +is no longer D valid code and that's why it is being deprecated. + +Let's consider the following code: + +--- +struct Dog +{ + this() {} + this(string name = "doggie") {} // [warn]: This struct constructor can never be called with its default argument. +} +--- + +D-Scanner would throw and error for this particular struct, but this code +does not compile anymore hence this check is not needed anymore/ \ No newline at end of file diff --git a/dmd b/dmd new file mode 160000 index 00000000..a4cbc08f --- /dev/null +++ b/dmd @@ -0,0 +1 @@ +Subproject commit a4cbc08f5bc1a2f7ce3289103198c473671e94c0 diff --git a/dub.json b/dub.json index e681e863..6331b05a 100644 --- a/dub.json +++ b/dub.json @@ -15,7 +15,11 @@ "dcd:dsymbol": ">=0.16.0-beta.2 <0.17.0", "inifiled": "~>1.3.1", "emsi_containers": "~>0.9.0", - "libddoc": "~>0.8.0" + "libddoc": "~>0.8.0", + "dmd": { + "repository": "git+https://github.com/dlang/dmd.git", + "version": "a4cbc08f5bc1a2f7ce3289103198c473671e94c0" + } }, "targetPath" : "bin", "stringImportPaths" : [ diff --git a/dub.selections.json b/dub.selections.json index 5e58ee4f..dd3c5923 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -2,6 +2,7 @@ "fileVersion": 1, "versions": { "dcd": "0.16.0-beta.2", + "dmd": "~master", "dsymbol": "0.13.0", "emsi_containers": "0.9.0", "inifiled": "1.3.3", diff --git a/makefile b/makefile index 4d6dcedd..b7b696b4 100644 --- a/makefile +++ b/makefile @@ -1,11 +1,45 @@ .PHONY: all test clean +.DEFAULT_GOAL := all + DC ?= dmd GIT ?= git DMD := $(DC) GDC := gdc LDC := ldc2 +DMD_FRONTEND_SRC := \ + $(shell find dmd/compiler/src/dmd/common -name "*.d")\ + $(shell find dmd/compiler/src/dmd/root -name "*.d")\ + $(shell find dmd/compiler/src/dmd/visitor -name "*.d")\ + $(shell find dmd/compiler/src/dmd/mangle -name "*.d")\ + $(shell find dmd/compiler/src/dmd -maxdepth 1 -name "*.d" \ + ! -name "mars.d" \ + ! -name "dmsc.d" \ + ! -name "e2ir.d" \ + ! -name "eh.d" \ + ! -name "glue.d" \ + ! -name "iasmdmd.d" \ + ! -name "irstate.d" \ + ! -name "lib.d" \ + ! -name "libelf.d" \ + ! -name "libmach.d" \ + ! -name "libmscoff.d" \ + ! -name "libomf.d" \ + ! -name "objc_glue.d" \ + ! -name "s2ir.d" \ + ! -name "scanelf.d" \ + ! -name "scanmach.d" \ + ! -name "scanmscoff.d" \ + ! -name "scanomf.d" \ + ! -name "tocsym.d" \ + ! -name "toctype.d" \ + ! -name "tocvdebug.d" \ + ! -name "toobj.d" \ + ! -name "todt.d" \ + ! -name "toir.d" \ + ) + LIB_SRC := \ $(shell find containers/src -name "*.d")\ $(shell find DCD/dsymbol/src -name "*.d")\ @@ -13,7 +47,9 @@ LIB_SRC := \ $(shell find libdparse/src/std/experimental/ -name "*.d")\ $(shell find libdparse/src/dparse/ -name "*.d")\ $(shell find libddoc/src -name "*.d") \ - $(shell find libddoc/common/source -name "*.d") + $(shell find libddoc/common/source -name "*.d") \ + $(DMD_FRONTEND_SRC) + PROJECT_SRC := $(shell find src/ -name "*.d") SRC := $(LIB_SRC) $(PROJECT_SRC) @@ -42,57 +78,72 @@ INCLUDE_PATHS = \ -IDCD/dsymbol/src \ -Icontainers/src \ -Ilibddoc/src \ - -Ilibddoc/common/source + -Ilibddoc/common/source \ + -Idmd/compiler/src -# e.g. "-version=MyCustomVersion" -DMD_VERSIONS = +DMD_VERSIONS = -version=StdLoggerDisableWarning -version=CallbackAPI -version=DMDLIB -version=MARS -version=NoBackend -version=NoMain DMD_DEBUG_VERSIONS = -version=dparse_verbose -# e.g. "-d-version=MyCustomVersion" -LDC_VERSIONS = +LDC_VERSIONS = -d-version=StdLoggerDisableWarning -d-version=CallbackAPI -d-version=DMDLIB -d-version=MARS -d-version=NoBackend -d-version=NoMain LDC_DEBUG_VERSIONS = -d-version=dparse_verbose -# e.g. "-fversion=MyCustomVersion" -GDC_VERSIONS = +GDC_VERSIONS = -fversion=StdLoggerDisableWarning -fversion=CallbackAPI -fversion=DMDLIB -fversion=MARS -fversion=NoBackend -fversion=NoMain GDC_DEBUG_VERSIONS = -fversion=dparse_verbose -DC_FLAGS += -Jbin +DC_FLAGS += -Jbin -Jdmd -Jdmd/compiler/src/dmd/res override DMD_FLAGS += $(DFLAGS) -w -release -O -od${OBJ_DIR} override LDC_FLAGS += $(DFLAGS) -O5 -release -oq override GDC_FLAGS += $(DFLAGS) -O3 -frelease -fall-instantiations override GDC_TEST_FLAGS += -fall-instantiations -DC_TEST_FLAGS += -g -Jbin +DC_TEST_FLAGS += -g -Jbin -Jdmd -Jdmd/compiler/src/dmd/res override DMD_TEST_FLAGS += -w -DC_DEBUG_FLAGS := -g -Jbin +DC_DEBUG_FLAGS := -g -Jbin -Jdmd -Jdmd/compiler/src/dmd/res ifeq ($(DC), $(filter $(DC), dmd ldmd2 gdmd)) VERSIONS := $(DMD_VERSIONS) DEBUG_VERSIONS := $(DMD_DEBUG_VERSIONS) DC_FLAGS += $(DMD_FLAGS) DC_TEST_FLAGS += $(DMD_TEST_FLAGS) -unittest + DC_DEBUG_FLAGS += -O WRITE_TO_TARGET_NAME = -of$@ else ifneq (,$(findstring ldc2, $(DC))) VERSIONS := $(LDC_VERSIONS) DEBUG_VERSIONS := $(LDC_DEBUG_VERSIONS) DC_FLAGS += $(LDC_FLAGS) DC_TEST_FLAGS += $(LDC_TEST_FLAGS) -unittest + DC_DEBUG_FLAGS += -O WRITE_TO_TARGET_NAME = -of=$@ else ifneq (,$(findstring gdc, $(DC))) VERSIONS := $(GDC_VERSIONS) DEBUG_VERSIONS := $(GDC_DEBUG_VERSIONS) DC_FLAGS += $(GDC_FLAGS) DC_TEST_FLAGS += $(GDC_TEST_FLAGS) -funittest + DC_DEBUG_FLAGS += -O3 -fall-instantiations WRITE_TO_TARGET_NAME = -o $@ endif +SHELL:=/usr/bin/env bash GITHASH = bin/githash.txt +FIRST_RUN_FLAG := bin/first_run.flag + +ifneq (, $(findstring $(GDC), $(DC))) + CONFIG_CMD := $(DC) dmd/config.d -o config && ./config bin VERSION /etc && rm config; + else + CONFIG_CMD := $(DC) -run dmd/config.d bin VERSION /etc; + endif + +$(FIRST_RUN_FLAG): + if [ ! -f $(FIRST_RUN_FLAG) ]; then \ + $(CONFIG_CMD) \ + touch $(FIRST_RUN_FLAG); \ + fi -$(OBJ_DIR)/$(DC)/%.o: %.d +$(OBJ_DIR)/$(DC)/%.o: %.d | ${FIRST_RUN_FLAG} ${DC} ${DC_FLAGS} ${VERSIONS} ${INCLUDE_PATHS} -c $< ${WRITE_TO_TARGET_NAME} -$(UT_OBJ_DIR)/$(DC)/%.o: %.d +$(UT_OBJ_DIR)/$(DC)/%.o: %.d | ${FIRST_RUN_FLAG} ${DC} ${DC_TEST_FLAGS} ${VERSIONS} ${INCLUDE_PATHS} -c $< ${WRITE_TO_TARGET_NAME} ${DSCANNER_BIN}: ${GITHASH} ${OBJ_BY_DC} | ${DSCANNER_BIN_DIR} @@ -132,7 +183,7 @@ ${UT_DSCANNER_LIB}: ${LIB_SRC} | ${UT_DSCANNER_LIB_DIR} test: ${UT_DSCANNER_BIN} -${UT_DSCANNER_BIN}: ${UT_DSCANNER_LIB} ${GITHASH} ${UT_OBJ_BY_DC} | ${DSCANNER_BIN_DIR} +${UT_DSCANNER_BIN}: ${GITHASH} ${UT_OBJ_BY_DC} ${UT_DSCANNER_LIB} | ${DSCANNER_BIN_DIR} ${DC} ${UT_DSCANNER_LIB} ${UT_OBJ_BY_DC} ${WRITE_TO_TARGET_NAME} ./${UT_DSCANNER_BIN} @@ -151,3 +202,42 @@ report: all release: ./release.sh + +# Add source files here as we transition to DMD-as-a-library +STYLE_CHECKED_SRC := \ + src/dscanner/imports.d \ + src/dscanner/main.d + +style: + @echo "Check for trailing whitespace" + grep -nr '[[:blank:]]$$' ${STYLE_CHECKED_SRC}; test $$? -eq 1 + + @echo "Enforce whitespace before opening parenthesis" + grep -nrE "\<(for|foreach|foreach_reverse|if|while|switch|catch|version)\(" ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce no whitespace after opening parenthesis" + grep -nrE "\<(version) \( " ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce whitespace between colon(:) for import statements (doesn't catch everything)" + grep -nr 'import [^/,=]*:.*;' ${STYLE_CHECKED_SRC} | grep -vE "import ([^ ]+) :\s"; test $$? -eq 1 + + @echo "Check for package wide std.algorithm imports" + grep -nr 'import std.algorithm : ' ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce Allman style" + grep -nrE '(if|for|foreach|foreach_reverse|while|unittest|switch|else|version) .*{$$' ${STYLE_CHECKED_SRC}; test $$? -eq 1 + + @echo "Enforce do { to be in Allman style" + grep -nr 'do *{$$' ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce no space between assert and the opening brace, i.e. assert(" + grep -nrE 'assert +\(' ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce space after cast(...)" + grep -nrE '[^"]cast\([^)]*?\)[[:alnum:]]' ${STYLE_CHECKED_SRC} ; test $$? -eq 1 + + @echo "Enforce space between a .. b" + grep -nrE '[[:alnum:]][.][.][[:alnum:]]|[[:alnum:]] [.][.][[:alnum:]]|[[:alnum:]][.][.] [[:alnum:]]' ${STYLE_CHECKED_SRC}; test $$? -eq 1 + + @echo "Enforce space between binary operators" + grep -nrE "[[:alnum:]](==|!=|<=|<<|>>|>>>|^^)[[:alnum:]]|[[:alnum:]] (==|!=|<=|<<|>>|>>>|^^)[[:alnum:]]|[[:alnum:]](==|!=|<=|<<|>>|>>>|^^) [[:alnum:]]" ${STYLE_CHECKED_SRC}; test $$? -eq 1 diff --git a/src/dscanner/analysis/alias_syntax_check.d b/src/dscanner/analysis/alias_syntax_check.d index 5c30ec44..7629a496 100644 --- a/src/dscanner/analysis/alias_syntax_check.d +++ b/src/dscanner/analysis/alias_syntax_check.d @@ -5,50 +5,80 @@ module dscanner.analysis.alias_syntax_check; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; +import dmd.tokens; +import dmd.lexer : Lexer; +import dmd.location : Loc; /** * Checks for uses of the old alias syntax. */ -final class AliasSyntaxCheck : BaseAnalyzer +extern(C++) class AliasSyntaxCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - mixin AnalyzerInfo!"alias_syntax_check"; + alias visit = BaseAnalyzerDmd.visit; - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); } - override void visit(const AliasDeclaration ad) + override void visit(AST.AliasDeclaration ad) { - if (ad.declaratorIdentifierList is null) - return; - assert(ad.declaratorIdentifierList.identifiers.length > 0, - "Identifier list length is zero, libdparse has a bug"); - addErrorMessage(ad, KEY, - "Prefer the new \"'alias' identifier '=' type ';'\" syntax" - ~ " to the old \"'alias' type identifier ';'\" syntax."); + import dscanner.utils: readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + auto bytes = readFile(fileName); + bool foundEq = false; + Loc idLoc; + + bytes ~= '\0'; + bytes = bytes[ad.loc.fileOffset .. $]; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + TOK nextTok; + lexer.nextToken(); + + do + { + if (lexer.token.value == TOK.assign) + foundEq = true; + + if (lexer.token.value == TOK.identifier) + idLoc = lexer.token.loc; + + nextTok = lexer.nextToken; + } + while(nextTok != TOK.semicolon && nextTok != TOK.endOfFile); + + if (!foundEq) + // Re-lexing is done based on offsets, so the alias appears to be at line 1. + // Fix this by computing the initial location. + addErrorMessage(cast(ulong) (ad.loc.linnum + idLoc.linnum - 1), cast(ulong) idLoc.charnum, KEY, + "Prefer the new \"'alias' identifier '=' type ';'\" syntax" + ~ " to the old \"'alias' type identifier ';'\" syntax."); } + private: enum KEY = "dscanner.style.alias_syntax"; } unittest { - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.alias_syntax_check = Check.enabled; assertAnalyzerWarnings(q{ - alias int abcde; /+ - ^^^^^^^^^^^^^^^^ [warn]: Prefer the new "'alias' identifier '=' type ';'" syntax to the old "'alias' type identifier ';'" syntax.+/ + alias int abcde; // [warn]: Prefer the new "'alias' identifier '=' type ';'" syntax to the old "'alias' type identifier ';'" syntax. alias abcde = int; }c, sac); diff --git a/src/dscanner/analysis/allman.d b/src/dscanner/analysis/allman.d index ace6ddd5..6a4b9bfd 100644 --- a/src/dscanner/analysis/allman.d +++ b/src/dscanner/analysis/allman.d @@ -4,13 +4,10 @@ module dscanner.analysis.allman; -import dparse.lexer; -import dparse.ast; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; - -import std.algorithm; -import std.range; +import dmd.tokens : Token, TOK; +import std.algorithm : canFind, until; +import std.range : retro; /** Checks for the allman style (braces should be on their own line) @@ -25,50 +22,85 @@ if (param < 0) } ------------ */ -final class AllManCheck : BaseAnalyzer +extern (C++) class AllManCheck : BaseAnalyzerDmd { mixin AnalyzerInfo!"allman_braces_check"; - /// - this(BaseAnalyzerArguments args) + private enum string KEY = "dscanner.style.allman"; + private enum string MESSAGE = "Braces should be on their own line"; + + private Token[] tokens; + + extern (D) this(string fileName, bool skipTests = false) + { + super(fileName, skipTests); + lexFile(); + checkBraces(); + } + + private void lexFile() + { + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + + do + { + lexer.nextToken(); + tokens ~= lexer.token; + } + while (lexer.token.value != TOK.endOfFile); + } + + private void checkBraces() { - super(args); foreach (i; 1 .. tokens.length - 1) { - const curLine = tokens[i].line; - const prevTokenLine = tokens[i-1].line; - if (tokens[i].type == tok!"{" && curLine == prevTokenLine) + const curLine = tokens[i].loc.linnum; + const prevTokenLine = tokens[i - 1].loc.linnum; + + if (tokens[i].value == TOK.leftCurly && curLine == prevTokenLine) { // ignore struct initialization - if (tokens[i-1].type == tok!"=") + if (tokens[i - 1].value == TOK.assign) continue; + // ignore duplicate braces - if (tokens[i-1].type == tok!"{" && tokens[i - 2].line != curLine) + if (tokens[i - 1].value == TOK.leftCurly && tokens[i - 2].loc.linnum != curLine) continue; + // ignore inline { } braces - if (curLine != tokens[i + 1].line) - addErrorMessage(tokens[i], KEY, MESSAGE); + if (curLine != tokens[i + 1].loc.linnum) + addErrorMessage(cast(ulong) tokens[i].loc.linnum, cast(ulong) tokens[i].loc.charnum, KEY, MESSAGE); } - if (tokens[i].type == tok!"}" && curLine == prevTokenLine) + + if (tokens[i].value == TOK.rightCurly && curLine == prevTokenLine) { // ignore duplicate braces - if (tokens[i-1].type == tok!"}" && tokens[i - 2].line != curLine) + if (tokens[i-1].value == TOK.rightCurly && tokens[i - 2].loc.linnum != curLine) continue; + // ignore inline { } braces - if (!tokens[0 .. i].retro.until!(t => t.line != curLine).canFind!(t => t.type == tok!"{")) - addErrorMessage(tokens[i], KEY, MESSAGE); + if (!tokens[0 .. i].retro.until!(t => t.loc.linnum != curLine).canFind!(t => t.value == TOK.leftCurly)) + addErrorMessage(cast(ulong) tokens[i].loc.linnum, cast(ulong) tokens[i].loc.charnum, KEY, MESSAGE); } } } - - enum string KEY = "dscanner.style.allman"; - enum string MESSAGE = "Braces should be on their own line"; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.format : format; import std.stdio : stderr; @@ -76,11 +108,10 @@ unittest sac.allman_braces_check = Check.enabled; // check common allman style violation - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testAllman() { - while (true) { /+ - ^ [warn]: %s +/ + while (true) { // [warn]: %s auto f = 1; } @@ -128,7 +159,7 @@ unittest ), sac); // check struct initialization - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ unittest { struct Foo { int a; } @@ -139,12 +170,11 @@ unittest }, sac); // allow duplicate braces - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ unittest {{ }} }, sac); - stderr.writeln("Unittest for Allman passed."); } diff --git a/src/dscanner/analysis/always_curly.d b/src/dscanner/analysis/always_curly.d index e320fcd1..3e139141 100644 --- a/src/dscanner/analysis/always_curly.d +++ b/src/dscanner/analysis/always_curly.d @@ -4,179 +4,229 @@ module dscanner.analysis.always_curly; -import dparse.lexer; -import dparse.ast; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; -import std.array : back, front; -import std.algorithm; -import std.range; -import std.stdio; - -final class AlwaysCurlyCheck : BaseAnalyzer +extern (C++) class AlwaysCurlyCheck(AST) : BaseAnalyzerDmd { + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"always_curly_check"; - alias visit = BaseAnalyzer.visit; + private enum string KEY = "dscanner.style.always_curly"; + private enum string MESSAGE_POSTFIX = " must be follow by a BlockStatement aka. { }"; + + private bool hasCurlyBraces; + private bool inCurlyStatement; /// - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - void test(L, B)(L loc, B s, string stmtKind) + mixin VisitBraceStatement!(AST.IfStatement, "if"); + mixin VisitBraceStatement!(AST.ForStatement, "for"); + mixin VisitBraceStatement!(AST.ForeachStatement, "foreach"); + mixin VisitBraceStatement!(AST.ForeachRangeStatement, "foreach"); + mixin VisitBraceStatement!(AST.WhileStatement, "while"); + mixin VisitBraceStatement!(AST.DoStatement, "do"); + mixin VisitBraceStatement!(AST.Catch, "catch"); + + private template VisitBraceStatement(NodeType, string nodeName) { - if (!is(s == BlockStatement)) + override void visit(NodeType node) { - if (!s.tokens.empty) - { - AutoFix af = AutoFix.insertionBefore(s.tokens.front, " { ") - .concat(AutoFix.insertionAfter(s.tokens.back, " } ")); - af.name = "Wrap in braces"; - - addErrorMessage(loc, KEY, stmtKind ~ MESSAGE_POSTFIX, [af]); - } + import dmd.hdrgen : toChars; + import std.conv : to; + import std.string : indexOf; + + auto oldHasCurlyBraces = hasCurlyBraces; + auto oldInCurlyStatement = inCurlyStatement; + hasCurlyBraces = false; + inCurlyStatement = true; + super.visit(node); + + static if (is(NodeType == AST.IfStatement)) + auto stmtBody = node.ifbody; + else static if (is(NodeType == AST.Catch)) + auto stmtBody = node.handler; else + auto stmtBody = node._body; + + if (!hasCurlyBraces) { - addErrorMessage(loc, KEY, stmtKind ~ MESSAGE_POSTFIX); + auto msg = nodeName ~ MESSAGE_POSTFIX; + string exprStr = to!string(toChars(stmtBody)); + + addErrorMessage( + node.loc.linnum, node.loc.charnum, KEY, msg, + [ + AutoFix.insertionAt(stmtBody.loc.fileOffset, "{ ") + .concat(AutoFix.insertionAt(stmtBody.loc.fileOffset + indexOf(exprStr, ';'), " }")) + ] + ); } - } - } - override void visit(const(IfStatement) stmt) - { - auto s = stmt.thenStatement.statement; - this.test(stmt.thenStatement, s, "if"); - if (stmt.elseStatement !is null) - { - auto e = stmt.elseStatement.statement; - this.test(stmt.elseStatement, e, "else"); + hasCurlyBraces = oldHasCurlyBraces; + inCurlyStatement = oldInCurlyStatement; } } - override void visit(const(ForStatement) stmt) + override void visit(AST.CompoundStatement cs) { - auto s = stmt.declarationOrStatement; - if (s.statement !is null) - { - this.test(s, s, "for"); - } + if (inCurlyStatement) + hasCurlyBraces = true; + super.visit(cs); } - override void visit(const(ForeachStatement) stmt) + override void visit(AST.TryCatchStatement tryCatchStatement) { - auto s = stmt.declarationOrStatement; - if (s.statement !is null) - { - this.test(s, s, "foreach"); - } + auto oldHasCurlyBraces = hasCurlyBraces; + auto oldInCurlyStatement = inCurlyStatement; + hasCurlyBraces = false; + inCurlyStatement = true; + + checkStatement(tryCatchStatement._body, "try"); + + hasCurlyBraces = oldHasCurlyBraces; + inCurlyStatement = oldInCurlyStatement; + + foreach (catchStatement; *tryCatchStatement.catches) + visit(catchStatement); } - override void visit(const(TryStatement) stmt) + override void visit(AST.TryFinallyStatement tryFinallyStatement) { - auto s = stmt.declarationOrStatement; - if (s.statement !is null) - { - this.test(s, s, "try"); - } + auto oldHasCurlyBraces = hasCurlyBraces; + auto oldInCurlyStatement = inCurlyStatement; - if (stmt.catches !is null) + if (tryFinallyStatement._body.isTryCatchStatement()) { - foreach (const(Catch) ct; stmt.catches.catches) - { - this.test(ct, ct.declarationOrStatement, "catch"); - } - if (stmt.catches.lastCatch !is null) - { - auto sncnd = stmt.catches.lastCatch.statementNoCaseNoDefault; - if (sncnd !is null) - { - this.test(stmt.catches.lastCatch, sncnd, "finally"); - } - } + tryFinallyStatement._body.accept(this); } - } - - override void visit(const(WhileStatement) stmt) - { - auto s = stmt.declarationOrStatement; - if (s.statement !is null) + else { - this.test(s, s, "while"); + hasCurlyBraces = false; + inCurlyStatement = true; + checkStatement(tryFinallyStatement._body, "try"); } + + hasCurlyBraces = false; + inCurlyStatement = true; + checkStatement(tryFinallyStatement.finalbody, "finally"); + + hasCurlyBraces = oldHasCurlyBraces; + inCurlyStatement = oldInCurlyStatement; } - override void visit(const(DoStatement) stmt) + extern (D) private void checkStatement(AST.Statement statement, string statementName) { - auto s = stmt.statementNoCaseNoDefault; - if (s !is null) + statement.accept(this); + + if (!hasCurlyBraces) { - this.test(s, s, "do"); + auto msg = statementName ~ MESSAGE_POSTFIX; + addErrorMessage(statement.loc.linnum, statement.loc.charnum, KEY, msg); } } - - enum string KEY = "dscanner.style.always_curly"; - enum string MESSAGE_POSTFIX = " must be follow by a BlockStatement aka. { }"; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.always_curly_check = Check.enabled; - - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testIf() { - if(true) return; // [warn]: if must be follow by a BlockStatement aka. { } + if (true) + { + return; + } + + if (true) return; // [warn]: if must be follow by a BlockStatement aka. { } } }, sac); - assertAnalyzerWarnings(q{ - void testIf() + assertAnalyzerWarningsDMD(q{ + void testFor() { - if(true) return; /+ - ^^^^^^^ [warn]: if must be follow by a BlockStatement aka. { } +/ + for (int i = 0; i < 10; ++i) + { + return; + } + + for (int i = 0; i < 10; ++i) return; // [warn]: for must be follow by a BlockStatement aka. { } } }, sac); - assertAnalyzerWarnings(q{ - void testIf() + assertAnalyzerWarningsDMD(q{ + void testForEach() { - for(int i = 0; i < 10; ++i) return; // [warn]: for must be follow by a BlockStatement aka. { } + foreach (it; 0 .. 10) + { + return; + } + + foreach (it; 0 .. 10) return; // [warn]: foreach must be follow by a BlockStatement aka. { } } }, sac); - assertAnalyzerWarnings(q{ - void testIf() + assertAnalyzerWarningsDMD(q{ + void testWhile() { - foreach(it; 0 .. 10) return; // [warn]: foreach must be follow by a BlockStatement aka. { } + while (true) + { + return; + } + + while (true) return; // [warn]: while must be follow by a BlockStatement aka. { } } }, sac); - assertAnalyzerWarnings(q{ - void testIf() + assertAnalyzerWarningsDMD(q{ + void testDoWhile() { - while(true) return; // [warn]: while must be follow by a BlockStatement aka. { } + do + { + return; + } while (true); + + do return; while (true); return; // [warn]: do must be follow by a BlockStatement aka. { } } }, sac); - assertAnalyzerWarnings(q{ - void testIf() + assertAnalyzerWarningsDMD(q{ + void testTryCatchFinally() { - do return; while(true); return; // [warn]: do must be follow by a BlockStatement aka. { } + try + { + return; + } + catch (Exception e) + { + return; + } + finally + { + return; + } + + try return; // [warn]: try must be follow by a BlockStatement aka. { } + catch (Exception e) return; // [warn]: catch must be follow by a BlockStatement aka. { } + finally return; // [warn]: finally must be follow by a BlockStatement aka. { } } - }, sac); + }c, sac); + + stderr.writeln("Unittest for AutoFix AlwaysCurly passed."); } -unittest { +unittest +{ import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); @@ -214,14 +264,14 @@ unittest { assertAutoFix(q{ void test() { - do return; while(true) // fix:0 + do return; while(true); // fix:0 } }c, q{ void test() { - do { return; } while(true) // fix:0 + do { return; } while(true); // fix:0 } }c, sac); - stderr.writeln("Unittest for AlwaysCurly passed."); + stderr.writeln("Unittest for AutoFix AlwaysCurly passed."); } diff --git a/src/dscanner/analysis/asm_style.d b/src/dscanner/analysis/asm_style.d index 73a6e532..7c7d12ed 100644 --- a/src/dscanner/analysis/asm_style.d +++ b/src/dscanner/analysis/asm_style.d @@ -6,39 +6,58 @@ module dscanner.analysis.asm_style; import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; +import dmd.tokens; /** * Checks for confusing asm expressions. * See_also: $(LINK https://issues.dlang.org/show_bug.cgi?id=9738) */ -final class AsmStyleCheck : BaseAnalyzer +extern (C++) class AsmStyleCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"asm_style_check"; - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const AsmBrExp brExp) + override void visit(AST.AsmStatement asmStatement) { - if (brExp.asmBrExp !is null && brExp.asmBrExp.asmUnaExp !is null - && brExp.asmBrExp.asmUnaExp.asmPrimaryExp !is null) + for (Token* token = asmStatement.tokens; token !is null; token = token.next) { - addErrorMessage(brExp, KEY, - "This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify."); + if (isConfusingStatement(token)) + { + auto lineNum = cast(size_t) token.loc.linnum; + auto charNum = cast(size_t) token.loc.charnum; + addErrorMessage(lineNum, charNum, KEY, MESSAGE); + } } - brExp.accept(this); } - private enum string KEY = "dscanner.confusing.brexp"; + private bool isConfusingStatement(Token* token) + { + if (token.next is null) + return false; + + if (token.next.next is null) + return false; + + TOK tok1 = token.value; + TOK tok2 = token.next.value; + TOK tok3 = token.next.next.value; + + if (tok1 == TOK.leftBracket && tok2 == TOK.int32Literal && tok3 == TOK.rightBracket) + return true; + + return false; + } + +private: + enum string KEY = "dscanner.confusing.brexp"; + enum string MESSAGE = "This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify."; } unittest @@ -47,13 +66,12 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.asm_style_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testAsm() { asm { - mov a, someArray[1]; /+ - ^^^^^^^^^^^^ [warn]: This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify. +/ + mov a, someArray[1]; // [warn]: This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify. add near ptr [EAX], 3; } } diff --git a/src/dscanner/analysis/assert_without_msg.d b/src/dscanner/analysis/assert_without_msg.d index 38246a32..3cf49d96 100644 --- a/src/dscanner/analysis/assert_without_msg.d +++ b/src/dscanner/analysis/assert_without_msg.d @@ -5,163 +5,69 @@ module dscanner.analysis.assert_without_msg; import dscanner.analysis.base; -import dscanner.utils : safeAccess; -import dsymbol.scope_ : Scope; -import dparse.lexer; -import dparse.ast; - import std.stdio; -import std.algorithm; /** * Check that all asserts have an explanatory message. */ -final class AssertWithoutMessageCheck : BaseAnalyzer +extern(C++) class AssertWithoutMessageCheck(AST) : BaseAnalyzerDmd { - enum string KEY = "dscanner.style.assert_without_msg"; - enum string MESSAGE = "An assert should have an explanatory message"; mixin AnalyzerInfo!"assert_without_msg"; + alias visit = BaseAnalyzerDmd.visit; /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const AssertExpression expr) + // Avoid visiting in/out contracts for this check + override void visitFuncBody(AST.FuncDeclaration f) { - static if (__traits(hasMember, expr.assertArguments, "messageParts")) - { - // libdparse 0.22.0+ - bool hasMessage = expr.assertArguments - && expr.assertArguments.messageParts.length > 0; - } - else - bool hasMessage = expr.assertArguments - && expr.assertArguments.message !is null; - - if (!hasMessage) - addErrorMessage(expr, KEY, MESSAGE); + if (f.fbody) + { + f.fbody.accept(this); + } } - override void visit(const FunctionCallExpression expr) + override void visit(AST.AssertExp ae) { - if (!isStdExceptionImported) - return; - - if (const IdentifierOrTemplateInstance iot = safeAccess(expr) - .unaryExpression.primaryExpression.identifierOrTemplateInstance) - { - auto ident = iot.identifier; - if (ident.text == "enforce" && expr.arguments !is null && expr.arguments.namedArgumentList !is null && - expr.arguments.namedArgumentList.items.length < 2) - addErrorMessage(expr, KEY, MESSAGE); - } + if (!ae.msg) + addErrorMessage(ae.loc.linnum, ae.loc.charnum, KEY, MESSAGE); } - override void visit(const SingleImport sImport) + override void visit(AST.StaticAssert ae) { - static immutable stdException = ["std", "exception"]; - if (sImport.identifierChain.identifiers.map!(a => a.text).equal(stdException)) - isStdExceptionImported = true; + if (!ae.msgs) + addErrorMessage(ae.loc.linnum, ae.loc.charnum, KEY, MESSAGE); } - // revert the stack after new scopes - override void visit(const Declaration decl) - { - // be careful - ImportDeclarations don't introduce a new scope - if (decl.importDeclaration is null) - { - bool tmp = isStdExceptionImported; - scope(exit) isStdExceptionImported = tmp; - decl.accept(this); - } - else - decl.accept(this); - } - - mixin ScopedVisit!IfStatement; - mixin ScopedVisit!BlockStatement; - - alias visit = BaseAnalyzer.visit; private: - bool isStdExceptionImported; - - template ScopedVisit(NodeType) - { - override void visit(const NodeType n) - { - bool tmp = isStdExceptionImported; - scope(exit) isStdExceptionImported = tmp; - n.accept(this); - } - } + enum string KEY = "dscanner.style.assert_without_msg"; + enum string MESSAGE = "An assert should have an explanatory message"; } unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.assert_without_msg = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ unittest { assert(0, "foo bar"); - assert(0); /+ - ^^^^^^^^^ [warn]: %s +/ + assert(0); // [warn]: An assert should have an explanatory message } - }c.format( - AssertWithoutMessageCheck.MESSAGE, - ), sac); + }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ unittest { static assert(0, "foo bar"); - static assert(0); /+ - ^^^^^^^^^ [warn]: %s +/ - } - }c.format( - AssertWithoutMessageCheck.MESSAGE, - ), sac); - - // check for std.exception.enforce - assertAnalyzerWarnings(q{ - unittest { - enforce(0); // std.exception not imported yet - could be a user-defined symbol - import std.exception; - enforce(0, "foo bar"); - enforce(0); /+ - ^^^^^^^^^^ [warn]: %s +/ - } - }c.format( - AssertWithoutMessageCheck.MESSAGE, - ), sac); - - // check for std.exception.enforce - assertAnalyzerWarnings(q{ - unittest { - import exception; - class C { - import std.exception; - } - enforce(0); // std.exception not imported yet - could be a user-defined symbol - struct S { - import std.exception; - } - enforce(0); // std.exception not imported yet - could be a user-defined symbol - if (false) { - import std.exception; - } - enforce(0); // std.exception not imported yet - could be a user-defined symbol - { - import std.exception; - } - enforce(0); // std.exception not imported yet - could be a user-defined symbol + static assert(0); // [warn]: An assert should have an explanatory message } }c, sac); diff --git a/src/dscanner/analysis/auto_function.d b/src/dscanner/analysis/auto_function.d index 07b47baf..4588621b 100644 --- a/src/dscanner/analysis/auto_function.d +++ b/src/dscanner/analysis/auto_function.d @@ -6,12 +6,8 @@ module dscanner.analysis.auto_function; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; - -import std.stdio; -import std.algorithm : map, filter; +import std.conv : to; +import std.algorithm.searching : canFind; /** * Checks for auto functions without return statement. @@ -20,166 +16,137 @@ import std.algorithm : map, filter; * detected by the compiler. However sometimes they can be used as a trick * to infer attributes. */ -final class AutoFunctionChecker : BaseAnalyzer +extern (C++) class AutoFunctionChecker(AST) : BaseAnalyzerDmd { + alias visit = BaseAnalyzerDmd.visit; + mixin AnalyzerInfo!"auto_function_check"; -private: - - enum string KEY = "dscanner.suspicious.missing_return"; - enum string MESSAGE = "Auto function without return statement, prefer replacing auto with void"; - enum string MESSAGE_INSERT = "Auto function without return statement, prefer inserting void to be explicit"; - - bool[] _returns; - size_t _mixinDepth; - string[] _literalWithReturn; - -public: - - alias visit = BaseAnalyzer.visit; + private bool foundReturn; + private bool foundFalseAssert; + private bool inMixin; + private bool foundReturnLiteral; + private string[] literalsWithReturn; - mixin AnalyzerInfo!"auto_function_check"; + private enum string KEY = "dscanner.suspicious.missing_return"; + private enum string MESSAGE = "Auto function without return statement, prefer replacing auto with void"; + private enum string MESSAGE_INSERT = "Auto function without return statement, prefer inserting void to be explicit"; /// - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - package static const(Token)[] findAutoReturnType(const(FunctionDeclaration) decl) + override void visit(AST.FuncDeclaration d) { - const(Token)[] lastAtAttribute; - foreach (storageClass; decl.storageClasses) - { - if (storageClass.token.type == tok!"auto") - return storageClass.tokens; - else if (storageClass.atAttribute) - lastAtAttribute = storageClass.atAttribute.tokens; - } - return lastAtAttribute; - } + import dmd.astenums : STC, STMT; - override void visit(const(FunctionDeclaration) decl) - { - _returns.length += 1; - scope(exit) _returns.length -= 1; - _returns[$-1] = false; + if (d.storage_class & STC.disable || d.fbody is null || (d.fbody && d.fbody.isReturnStatement())) + return; + + ulong lineNum = cast(ulong) d.loc.linnum; + ulong charNum = cast(ulong) d.loc.charnum; - auto autoTokens = findAutoReturnType(decl); - bool isAtAttribute = autoTokens.length > 1; + auto oldFoundReturn = foundReturn; + auto oldFoundFalseAssert = foundFalseAssert; - decl.accept(this); + foundReturn = false; + foundFalseAssert = false; + super.visitFuncBody(d); - if (decl.functionBody.specifiedFunctionBody && autoTokens.length && !_returns[$-1]) + if (!foundReturn && !foundFalseAssert) { - if (isAtAttribute) + if (d.storage_class & STC.auto_) { - // highlight on the whitespace between attribute and function name - auto tok = autoTokens[$ - 1]; - auto whitespace = tok.column + (tok.text.length ? tok.text.length : str(tok.type).length); - auto whitespaceIndex = tok.index + (tok.text.length ? tok.text.length : str(tok.type).length); - addErrorMessage([whitespaceIndex, whitespaceIndex + 1], tok.line, [whitespace, whitespace + 1], KEY, MESSAGE_INSERT, - [AutoFix.insertionAt(whitespaceIndex + 1, "void ")]); + auto voidStart = extractVoidStartLocation(d); + + addErrorMessage( + lineNum, charNum, KEY, MESSAGE, + [ + AutoFix.replacement(voidStart + 1, voidStart + 6, "", "Replace `auto` with `void`") + .concat(AutoFix.insertionAt(d.loc.fileOffset, "void ")) + ] + ); + } + else if (auto returnType = cast(AST.TypeFunction) d.type) + { + if (returnType.next is null) + { + addErrorMessage( + lineNum, charNum, KEY, MESSAGE_INSERT, + [AutoFix.insertionAt(d.loc.fileOffset, "void ")] + ); + } } - else - addErrorMessage(autoTokens, KEY, MESSAGE, - [AutoFix.replacement(autoTokens[0], "", "Replace `auto` with `void`") - .concat(AutoFix.insertionAt(decl.name.index, "void "))]); } - } - override void visit(const(ReturnStatement) rst) - { - if (_returns.length) - _returns[$-1] = true; - rst.accept(this); + foundReturn = oldFoundReturn; + foundFalseAssert = oldFoundFalseAssert; } - override void visit(const(AssertArguments) exp) + private auto extractVoidStartLocation(AST.FuncDeclaration d) { - exp.accept(this); - if (_returns.length) - { - const UnaryExpression u = cast(UnaryExpression) exp.assertion; - if (!u) - return; - const PrimaryExpression p = u.primaryExpression; - if (!p) - return; - - immutable token = p.primary; - if (token.type == tok!"false") - _returns[$-1] = true; - else if (token.text == "0") - _returns[$-1] = true; - } + import dmd.common.outbuffer : OutBuffer; + import dmd.hdrgen : toCBuffer, HdrGenState; + import std.string : indexOf; + + OutBuffer buf; + HdrGenState hgs; + toCBuffer(d, buf, hgs); + string funcStr = cast(string) buf.extractSlice(); + string funcName = cast(string) d.ident.toString(); + auto funcNameStart = funcStr.indexOf(funcName); + auto voidTokenStart = funcStr.indexOf("void"); + auto voidOffset = funcNameStart - voidTokenStart; + return d.loc.fileOffset - voidOffset; } - override void visit(const(MixinExpression) mix) + override void visit(AST.ReturnStatement s) { - ++_mixinDepth; - mix.accept(this); - --_mixinDepth; + foundReturn = true; } - override void visit(const(PrimaryExpression) exp) + override void visit(AST.AssertExp assertExpr) { - exp.accept(this); - - import std.algorithm.searching : canFind; - - if (_returns.length && _mixinDepth) - { - if (findReturnInLiteral(exp.primary.text)) - _returns[$-1] = true; - else if (exp.identifierOrTemplateInstance && - _literalWithReturn.canFind(exp.identifierOrTemplateInstance.identifier.text)) - _returns[$-1] = true; - } + auto ie = assertExpr.e1.isIntegerExp(); + if (ie && ie.getInteger() == 0) + foundFalseAssert = true; } - private bool findReturnInLiteral(const(string) value) + override void visit(AST.MixinStatement mixinStatement) { - import std.algorithm.searching : find; - import std.range : empty; - - return value == "return" || !value.find("return ").empty; + auto oldInMixin = inMixin; + inMixin = true; + super.visit(mixinStatement); + inMixin = oldInMixin; } - private bool stringliteralHasReturn(const(NonVoidInitializer) nvi) + override void visit(AST.StringExp stringExpr) { - bool result; - if (!nvi.assignExpression || (cast(UnaryExpression) nvi.assignExpression) is null) - return result; - - const(UnaryExpression) u = cast(UnaryExpression) nvi.assignExpression; - if (u.primaryExpression && - u.primaryExpression.primary.type.isStringLiteral && - findReturnInLiteral(u.primaryExpression.primary.text)) - result = true; + foundReturnLiteral = foundReturnLiteral || canFind(stringExpr.toStringz(), "return"); - return result; + if (inMixin) + foundReturn = foundReturn || foundReturnLiteral; } - override void visit(const(AutoDeclaration) decl) + override void visit(AST.IdentifierExp ie) { - decl.accept(this); + if (inMixin) + foundReturn = foundReturn || canFind(literalsWithReturn, to!string(ie.ident.toString())); - foreach(const(AutoDeclarationPart) p; decl.parts) - if (p.initializer && - p.initializer.nonVoidInitializer && - stringliteralHasReturn(p.initializer.nonVoidInitializer)) - _literalWithReturn ~= p.identifier.text.idup; + super.visit(ie); } - override void visit(const(VariableDeclaration) decl) + override void visit(AST.VarDeclaration vd) { - decl.accept(this); + auto oldFoundReturnLiteral = foundReturnLiteral; + foundFalseAssert = false; + super.visit(vd); - foreach(const(Declarator) d; decl.declarators) - if (d.initializer && - d.initializer.nonVoidInitializer && - stringliteralHasReturn(d.initializer.nonVoidInitializer)) - _literalWithReturn ~= d.name.text.idup; + if (foundReturnLiteral) + literalsWithReturn ~= to!string(vd.ident.toString()); + + foundReturnLiteral = oldFoundReturnLiteral; } } @@ -188,95 +155,63 @@ unittest import std.stdio : stderr; import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; StaticAnalysisConfig sac = disabledConfig(); sac.auto_function_check = Check.enabled; - assertAnalyzerWarnings(q{ - auto ref doStuff(){} /+ - ^^^^ [warn]: %s +/ - auto doStuff(){} /+ - ^^^^ [warn]: %s +/ + + string MESSAGE = "Auto function without return statement, prefer replacing auto with void"; + string MESSAGE_INSERT = "Auto function without return statement, prefer inserting void to be explicit"; + + assertAnalyzerWarningsDMD(q{ + auto ref doStuff(){} // [warn]: %s + auto doStuff(){} // [warn]: %s @Custom - auto doStuff(){} /+ - ^^^^ [warn]: %s +/ - int doStuff(){auto doStuff(){}} /+ - ^^^^ [warn]: %s +/ + auto doStuff(){} // [warn]: %s + int doStuff(){auto doStuff(){}} // [warn]: %s auto doStuff(){return 0;} int doStuff(){/*error but not the aim*/} - }c.format( - AutoFunctionChecker.MESSAGE, - AutoFunctionChecker.MESSAGE, - AutoFunctionChecker.MESSAGE, - AutoFunctionChecker.MESSAGE, - ), sac); - - assertAnalyzerWarnings(q{ - auto doStuff(){assert(true);} /+ - ^^^^ [warn]: %s +/ + }c.format(MESSAGE, MESSAGE, MESSAGE, MESSAGE), sac); + + assertAnalyzerWarningsDMD(q{ + auto doStuff(){assert(true);} // [warn]: %s auto doStuff(){assert(false);} - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); + }c.format(MESSAGE), sac); - assertAnalyzerWarnings(q{ - auto doStuff(){assert(1);} /+ - ^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + auto doStuff(){assert(1);} // [warn]: %s auto doStuff(){assert(0);} - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); + }c.format(MESSAGE), sac); - assertAnalyzerWarnings(q{ - auto doStuff(){mixin("0+0");} /+ - ^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + auto doStuff(){mixin("0+0");} // [warn]: %s auto doStuff(){mixin("return 0;");} - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); + }c.format(MESSAGE), sac); - assertAnalyzerWarnings(q{ - auto doStuff(){mixin("0+0");} /+ - ^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + auto doStuff(){mixin("0+0");} // [warn]: %s auto doStuff(){mixin("static if (true)" ~ " return " ~ 0.stringof ~ ";");} - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); - - assertAnalyzerWarnings(q{ - auto doStuff(){} /+ - ^^^^ [warn]: %s +/ - extern(C) auto doStuff(); - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); - - assertAnalyzerWarnings(q{ - auto doStuff(){} /+ - ^^^^ [warn]: %s +/ - @disable auto doStuff(); - }c.format( - AutoFunctionChecker.MESSAGE, - ), sac); - - assertAnalyzerWarnings(q{ - @property doStuff(){} /+ - ^ [warn]: %s +/ - @safe doStuff(){} /+ - ^ [warn]: %s +/ - @disable doStuff(); + }c.format(MESSAGE), sac); + + assertAnalyzerWarningsDMD(q{ + auto doStuff(){} // [warn]: %s + }c.format(MESSAGE), sac); + + assertAnalyzerWarningsDMD(q{ + auto doStuff(){} // [warn]: %s + }c.format(MESSAGE), sac); + + assertAnalyzerWarningsDMD(q{ + @property doStuff(){} // [warn]: %s + @safe doStuff(){} // [warn]: %s @safe void doStuff(); - }c.format( - AutoFunctionChecker.MESSAGE_INSERT, - AutoFunctionChecker.MESSAGE_INSERT, - ), sac); + }c.format(MESSAGE_INSERT, MESSAGE_INSERT), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ enum _genSave = "return true;"; auto doStuff(){ mixin(_genSave);} }, sac); - assertAutoFix(q{ auto ref doStuff(){} // fix auto doStuff(){} // fix diff --git a/src/dscanner/analysis/auto_ref_assignment.d b/src/dscanner/analysis/auto_ref_assignment.d index acd272de..69d35a6f 100644 --- a/src/dscanner/analysis/auto_ref_assignment.d +++ b/src/dscanner/analysis/auto_ref_assignment.d @@ -5,127 +5,97 @@ module dscanner.analysis.auto_ref_assignment; -import dparse.lexer; -import dparse.ast; import dscanner.analysis.base; /** * Checks for assignment to auto-ref function parameters. */ -final class AutoRefAssignmentCheck : BaseAnalyzer +extern(C++) class AutoRefAssignmentCheck(AST) : BaseAnalyzerDmd { mixin AnalyzerInfo!"auto_ref_assignment_check"; + alias visit = BaseAnalyzerDmd.visit; - /// - this(BaseAnalyzerArguments args) - { - super(args); - } + mixin ScopedVisit!(AST.ClassDeclaration); + mixin ScopedVisit!(AST.StructDeclaration); + mixin ScopedVisit!(AST.FuncDeclaration); + mixin ScopedVisit!(AST.InterfaceDeclaration); + mixin ScopedVisit!(AST.UnionDeclaration); + mixin ScopedVisit!(AST.ScopeStatement); - override void visit(const Module m) + /// + extern(D) this(string fileName) { - pushScope(); - m.accept(this); - popScope(); + super(fileName); } - override void visit(const FunctionDeclaration func) + override void visit(AST.TemplateDeclaration td) { - if (func.parameters is null || func.parameters.parameters.length == 0) - return; - pushScope(); - scope (exit) - popScope(); - func.accept(this); + auto autoRefParamsOld = autoRefParams; + autoRefParams = []; + auto temp = inTemplateScope; + inTemplateScope = true; + + super.visit(td); + + inTemplateScope = temp; + autoRefParams = autoRefParamsOld; } - override void visit(const Parameter param) + override void visit(AST.Parameter p) { - import std.algorithm.searching : canFind; + import dmd.astenums : STC; - immutable bool isAuto = param.parameterAttributes.canFind!(a => a.idType == cast(ubyte) tok!"auto"); - immutable bool isRef = param.parameterAttributes.canFind!(a => a.idType == cast(ubyte) tok!"ref"); - if (!isAuto || !isRef) - return; - addSymbol(param.name.text); + if (p.storageClass & STC.auto_ && p.storageClass & STC.ref_ && p.ident) + autoRefParams ~= p.ident.toString(); } - override void visit(const AssignExpression assign) + override void visit(AST.AssignExp ae) { - if (assign.operator == tok!"" || scopes.length == 0) - return; - interest ~= assign; - assign.ternaryExpression.accept(this); - interest.length--; - } + import std.algorithm: canFind; - override void visit(const IdentifierOrTemplateInstance ioti) - { - import std.algorithm.searching : canFind; + auto ie = ae.e1.isIdentifierExp(); - if (ioti.identifier == tok!"" || !interest.length) - return; - if (scopes[$ - 1].canFind(ioti.identifier.text)) - addErrorMessage(interest[$ - 1], KEY, MESSAGE); + if (ie && inTemplateScope && autoRefParams.canFind(ie.ident.toString())) + addErrorMessage(cast(ulong) ae.loc.linnum, cast(ulong) ae.loc.charnum, KEY, + "Assignment to auto-ref function parameter."); } - override void visit(const IdentifierChain ic) + template ScopedVisit(NodeType) { - import std.algorithm.searching : canFind; - - if (ic.identifiers.length == 0 || !interest.length) - return; - if (scopes[$ - 1].canFind(ic.identifiers[0].text)) - addErrorMessage(interest[$ - 1], KEY, MESSAGE); + override void visit(NodeType n) + { + auto temp = inTemplateScope; + inTemplateScope = false; + super.visit(n); + inTemplateScope = temp; + } } - alias visit = BaseAnalyzer.visit; - private: + const(char[])[] autoRefParams; + bool inTemplateScope; - enum string MESSAGE = "Assignment to auto-ref function parameter."; - enum string KEY = "dscanner.suspicious.auto_ref_assignment"; - - const(AssignExpression)[] interest; - - void addSymbol(string symbolName) - { - scopes[$ - 1] ~= symbolName; - } - - void pushScope() - { - scopes.length++; - } - - void popScope() - { - scopes = scopes[0 .. $ - 1]; - } - - string[][] scopes; + enum KEY = "dscanner.suspicious.object_const"; } unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.auto_ref_assignment_check = Check.enabled; assertAnalyzerWarnings(q{ int doStuff(T)(auto ref int a) { - a = 10; /+ - ^^^^^^ [warn]: %s +/ + a = 10; // [warn]: Assignment to auto-ref function parameter. } int doStuff(T)(ref int a) { a = 10; } - }c.format(AutoRefAssignmentCheck.MESSAGE), sac); + }c, sac); stderr.writeln("Unittest for AutoRefAssignmentCheck passed."); } diff --git a/src/dscanner/analysis/autofix.d b/src/dscanner/analysis/autofix.d new file mode 100644 index 00000000..6ecb0949 --- /dev/null +++ b/src/dscanner/analysis/autofix.d @@ -0,0 +1,251 @@ +module dscanner.analysis.autofix; + +import std.algorithm : filter, findSplit; +import std.conv : to; +import std.file : exists, remove; +import std.functional : toDelegate; +import std.stdio; + +import dscanner.analysis.base : AutoFix, AutoFixFormatting, BaseAnalyzerDmd, Message; +import dscanner.analysis.config : StaticAnalysisConfig; +import dscanner.analysis.run : analyze, doNothing; +import dscanner.analysis.rundmd; +import dscanner.utils : getModuleName, readFile, readStdin; + +void listAutofixes( + StaticAnalysisConfig config, + string resolveMessage, + bool usingStdin, + string fileName +) +{ + import std.format : format; + import std.json : JSONValue; + + union RequestedLocation + { + struct + { + uint line, column; + } + ulong bytes; + } + + RequestedLocation req; + bool isBytes = resolveMessage[0] == 'b'; + if (isBytes) + req.bytes = resolveMessage[1 .. $].to!ulong; + else + { + auto parts = resolveMessage.findSplit(":"); + req.line = parts[0].to!uint; + req.column = parts[2].to!uint; + } + + bool matchesCursor(Message m) + { + return isBytes ? req.bytes >= m.startIndex && req.bytes <= m.endIndex + : req.line >= m.startLine && req.line <= m.endLine + && (req.line > m.startLine || req.column >= m.startColumn) + && (req.line < m.endLine || req.column <= m.endColumn); + } + + ubyte[] code; + if (usingStdin) + { + code = readStdin(); + fileName = "stdin.d"; + File f = File(fileName, "w"); + f.rawWrite(code); + f.close(); + } + else + { + code = readFile(fileName); + } + + auto dmdModule = parseDmdModule(fileName, cast(string) code); + auto moduleName = getModuleName(dmdModule.md); + auto messages = analyzeDmd(fileName, dmdModule, moduleName, config); + + with (stdout.lockingTextWriter) + { + put("["); + foreach (message; messages[].filter!matchesCursor) + { + foreach (i, autofix; message.autofixes) + { + put(i == 0 ? "\n" : ",\n"); + put("\t{\n"); + put(format!"\t\t\"name\": %s,\n"(JSONValue(autofix.name))); + put("\t\t\"replacements\": ["); + foreach (j, replacement; autofix.expectReplacements) + { + put(j == 0 ? "\n" : ",\n"); + put(format!"\t\t\t{\"range\": [%d, %d], \"newText\": %s}"( + replacement.range[0], + replacement.range[1], + JSONValue(replacement.newText))); + } + put("\n"); + put("\t\t]\n"); + put("\t}"); + } + } + put("\n]"); + } + stdout.flush(); + + if (usingStdin) + { + assert(exists(fileName)); + remove(fileName); + } +} + +void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements) +{ + import std.algorithm : endsWith, startsWith; + import std.ascii : isWhite; + import std.string : strip; + import std.utf : stride, strideBack; + + enum WS + { + none, tab, space, newline + } + + WS getWS(size_t i) + { + if (cast(ptrdiff_t) i < 0 || i >= code.length) + return WS.newline; + switch (code[i]) + { + case '\n': + case '\r': + return WS.newline; + case '\t': + return WS.tab; + case ' ': + return WS.space; + default: + return WS.none; + } + } + + foreach (ref replacement; replacements) + { + assert(replacement.range[0] >= 0 && replacement.range[0] < code.length + && replacement.range[1] >= 0 && replacement.range[1] < code.length + && replacement.range[0] <= replacement.range[1], + "trying to autofix whitespace on code that doesn't match with what the replacements were generated for"); + + void growRight() + { + // this is basically: replacement.range[1]++; + if (code[replacement.range[1] .. $].startsWith("\r\n")) + replacement.range[1] += 2; + else if (replacement.range[1] < code.length) + replacement.range[1] += code.stride(replacement.range[1]); + } + + void growLeft() + { + // this is basically: replacement.range[0]--; + if (code[0 .. replacement.range[0]].endsWith("\r\n")) + replacement.range[0] -= 2; + else if (replacement.range[0] > 0) + replacement.range[0] -= code.strideBack(replacement.range[0]); + } + + if (replacement.newText.strip.length) + { + if (replacement.newText.startsWith(" ")) + { + // we insert with leading space, but there is a space/NL/SOF before + // remove to-be-inserted space + if (getWS(replacement.range[0] - 1)) + replacement.newText = replacement.newText[1 .. $]; + } + if (replacement.newText.startsWith("]", ")")) + { + // when inserting `)`, consume regular space before + if (getWS(replacement.range[0] - 1) == WS.space) + growLeft(); + } + if (replacement.newText.endsWith(" ")) + { + // we insert with trailing space, but there is a space/NL/EOF after, chomp off + if (getWS(replacement.range[1])) + replacement.newText = replacement.newText[0 .. $ - 1]; + } + if (replacement.newText.endsWith("[", "(")) + { + if (getWS(replacement.range[1])) + growRight(); + } + } + else if (!replacement.newText.length) + { + // after removing code and ending up with whitespace on both sides, + // collapse 2 whitespace into one + switch (getWS(replacement.range[1])) + { + case WS.newline: + switch (getWS(replacement.range[0] - 1)) + { + case WS.newline: + // after removal we have NL ~ NL or SOF ~ NL, + // remove right NL + growRight(); + break; + case WS.space: + case WS.tab: + // after removal we have space ~ NL, + // remove the space + growLeft(); + break; + default: + break; + } + break; + case WS.space: + case WS.tab: + // for NL ~ space, SOF ~ space, space ~ space, tab ~ space, + // for NL ~ tab, SOF ~ tab, space ~ tab, tab ~ tab + // remove right space/tab + if (getWS(replacement.range[0] - 1)) + growRight(); + break; + default: + break; + } + } + } +} + +unittest +{ + import std.algorithm : sort; + + AutoFix.CodeReplacement r(int start, int end, string s) + { + return AutoFix.CodeReplacement([start, end], s); + } + + string test(string code, AutoFix.CodeReplacement[] replacements...) + { + replacements.sort!"a.range[0] < b.range[0]"; + improveAutoFixWhitespace(code, replacements); + foreach_reverse (r; replacements) + code = code[0 .. r.range[0]] ~ r.newText ~ code[r.range[1] .. $]; + return code; + } + + assert(test("import a;\nimport b;", r(0, 9, "")) == "import b;"); + assert(test("import a;\r\nimport b;", r(0, 9, "")) == "import b;"); + assert(test("import a;\nimport b;", r(8, 9, "")) == "import a\nimport b;"); + assert(test("import a;\nimport b;", r(7, 8, "")) == "import ;\nimport b;"); + assert(test("import a;\r\nimport b;", r(7, 8, "")) == "import ;\r\nimport b;"); + assert(test("a b c", r(2, 3, "")) == "a c"); +} diff --git a/src/dscanner/analysis/base.d b/src/dscanner/analysis/base.d index a9baca08..c8b31098 100644 --- a/src/dscanner/analysis/base.d +++ b/src/dscanner/analysis/base.d @@ -2,13 +2,17 @@ module dscanner.analysis.base; import dparse.ast; import dparse.lexer : IdType, str, Token, tok; -import dscanner.analysis.nolint; -import dsymbol.scope_ : Scope; import std.array; import std.container; import std.meta : AliasSeq; import std.string; import std.sumtype; +import dmd.attrib : UserAttributeDeclaration; +import dmd.visitor.transitive; +import dmd.visitor; +import dmd.func; +import core.stdc.string; +import std.conv : to; /// struct AutoFix @@ -67,29 +71,14 @@ struct AutoFix return ret; } - static AutoFix replacement(const Token token, string newText, string name = null) + static AutoFix replacement(size_t tokenStart, size_t tokenEnd, string newText, string name) { - if (!name.length) - { - auto text = token.text.length ? token.text : str(token.type); - if (newText.length) - name = "Replace `" ~ text ~ "` with `" ~ newText ~ "`"; - else - name = "Remove `" ~ text ~ "`"; - } - return replacement([token], newText, name); - } - - static AutoFix replacement(const BaseNode node, string newText, string name) - { - return replacement(node.tokens, newText, name); - } - - static AutoFix replacement(const Token[] tokens, string newText, string name) - in(tokens.length > 0, "must provide at least one token") - { - auto end = tokens[$ - 1].text.length ? tokens[$ - 1].text : str(tokens[$ - 1].type); - return replacement([tokens[0].index, tokens[$ - 1].index + end.length], newText, name); + AutoFix ret; + ret.name = name; + ret.replacements = [ + AutoFix.CodeReplacement([tokenStart, tokenEnd], newText) + ]; + return ret; } static AutoFix replacement(size_t[2] range, string newText, string name) @@ -102,17 +91,6 @@ struct AutoFix return ret; } - static AutoFix insertionBefore(const Token token, string content, string name = null) - { - return insertionAt(token.index, content, name); - } - - static AutoFix insertionAfter(const Token token, string content, string name = null) - { - auto tokenText = token.text.length ? token.text : str(token.type); - return insertionAt(token.index + tokenText.length, content, name); - } - static AutoFix insertionAt(size_t index, string content, string name = null) { assert(content.length > 0, "generated auto fix inserting text without content"); @@ -128,24 +106,6 @@ struct AutoFix return ret; } - static AutoFix indentLines(scope const(Token)[] tokens, const AutoFixFormatting formatting, string name = "Indent code") - { - CodeReplacement[] inserts; - size_t line = -1; - foreach (token; tokens) - { - if (line != token.line) - { - line = token.line; - inserts ~= CodeReplacement([token.index, token.index], formatting.indentation); - } - } - AutoFix ret; - ret.name = name; - ret.replacements = inserts; - return ret; - } - AutoFix concat(AutoFix other) const { import std.algorithm : sort; @@ -276,33 +236,6 @@ struct Message deprecated("Use startLine instead") alias line = startLine; deprecated("Use startColumn instead") alias column = startColumn; - static Diagnostic from(string fileName, const BaseNode node, string message) - { - return from(fileName, node !is null ? node.tokens : [], message); - } - - static Diagnostic from(string fileName, const Token token, string message) - { - auto text = token.text.length ? token.text : str(token.type); - return from(fileName, - [token.index, token.index + text.length], - token.line, - [token.column, token.column + text.length], - message); - } - - static Diagnostic from(string fileName, const Token[] tokens, string message) - { - auto start = tokens.length ? tokens[0] : Token.init; - auto end = tokens.length ? tokens[$ - 1] : Token.init; - auto endText = end.text.length ? end.text : str(end.type); - return from(fileName, - [start.index, end.index + endText.length], - [start.line, end.line], - [start.column, end.column + endText.length], - message); - } - static Diagnostic from(string fileName, size_t[2] index, size_t line, size_t[2] columns, string message) { return Message.Diagnostic(fileName, index[0], index[1], line, line, columns[0], columns[1], message); @@ -327,7 +260,17 @@ struct Message /// the `BaseAnalyzer.resolveAutoFix` method with. AutoFix[] autofixes; - deprecated this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null) + this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null) + { + diagnostic.fileName = fileName; + diagnostic.startLine = diagnostic.endLine = line; + diagnostic.startColumn = diagnostic.endColumn = column; + diagnostic.message = message; + this.key = key; + this.checkName = checkName; + } + + this(string fileName, size_t line, size_t column, string key = null, string message = null, string checkName = null, AutoFix[] autofixes = null) { diagnostic.fileName = fileName; diagnostic.startLine = diagnostic.endLine = line; @@ -335,6 +278,7 @@ struct Message diagnostic.message = message; this.key = key; this.checkName = checkName; + this.autofixes = autofixes; } this(Diagnostic diagnostic, string key = null, string checkName = null, AutoFix[] autofixes = null) @@ -361,539 +305,109 @@ enum comparitor = q{ a.startLine < b.startLine || (a.startLine == b.startLine && alias MessageSet = RedBlackTree!(Message, comparitor, true); +/** + * Should be present in all visitors to specify the name of the check + * done by a patricular visitor + */ mixin template AnalyzerInfo(string checkName) { enum string name = checkName; - override protected string getName() + extern (D) override protected string getName() { return name; } } -struct BaseAnalyzerArguments -{ - string fileName; - const(Token)[] tokens; - const Scope* sc; - bool skipTests = false; - - BaseAnalyzerArguments setSkipTests(bool v) - { - auto ret = this; - ret.skipTests = v; - return ret; - } -} - -abstract class BaseAnalyzer : ASTVisitor +/** + * Visitor that implements the AST traversal logic. + * Supports collecting error messages + */ +extern(C++) class BaseAnalyzerDmd : SemanticTimeTransitiveVisitor { -public: - deprecated("Don't use this constructor, use the one taking BaseAnalyzerArguments") - this(string fileName, const Scope* sc, bool skipTests = false) - { - BaseAnalyzerArguments args = { - fileName: fileName, - sc: sc, - skipTests: skipTests - }; - this(args); - } + alias visit = SemanticTimeTransitiveVisitor.visit; - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - this.sc = args.sc; - this.tokens = args.tokens; - this.fileName = args.fileName; - this.skipTests = args.skipTests; + this.fileName = fileName; + this.skipTests = skipTests; _messages = new MessageSet; } - string getName() + /** + * Ensures that template AnalyzerInfo is instantiated in all classes + * deriving from this class + */ + extern(D) string getName() { assert(0); } - Message[] messages() + extern(D) Message[] messages() { return _messages[].array; } - alias visit = ASTVisitor.visit; - - /** - * Visits a unittest. - * - * When overriden, the protected bool "skipTests" should be handled - * so that the content of the test is not analyzed. - */ - override void visit(const Unittest unittest_) + override void visit(UnitTestDeclaration ud) { if (!skipTests) - unittest_.accept(this); - } - - /** - * Visits a module declaration. - * - * When overriden, make sure to keep this structure - */ - override void visit(const(Module) mod) - { - if (mod.moduleDeclaration !is null) - { - with (noLint.push(NoLintFactory.fromModuleDeclaration(mod.moduleDeclaration))) - mod.accept(this); - } - else - { - mod.accept(this); - } + super.visit(ud); } - /** - * Visits a declaration. - * - * When overriden, make sure to disable and reenable error messages - */ - override void visit(const(Declaration) decl) - { - with (noLint.push(NoLintFactory.fromDeclaration(decl))) - decl.accept(this); - } - - AutoFix.CodeReplacement[] resolveAutoFix( - const Module mod, - scope const(Token)[] tokens, - const AutoFix.ResolveContext context, - const AutoFixFormatting formatting, - ) - { - cast(void) mod; - cast(void) tokens; - cast(void) context; - cast(void) formatting; - assert(0); - } protected: - bool inAggregate; - bool skipTests; - const(Token)[] tokens; - NoLint noLint; - - template visitTemplate(T) - { - override void visit(const T structDec) - { - inAggregate = true; - structDec.accept(this); - inAggregate = false; - } - } - - deprecated("Use the overload taking start and end locations or a Node instead") - void addErrorMessage(size_t line, size_t column, string key, string message) + extern (D) void addErrorMessage(size_t line, size_t column, string key, string message) { - if (noLint.containsCheck(key)) - return; _messages.insert(Message(fileName, line, column, key, message, getName())); } - void addErrorMessage(const BaseNode node, string key, string message, AutoFix[] autofixes = null) - { - if (noLint.containsCheck(key)) - return; - addErrorMessage(Message.Diagnostic.from(fileName, node, message), key, autofixes); - } - - void addErrorMessage(const Token token, string key, string message, AutoFix[] autofixes = null) - { - if (noLint.containsCheck(key)) - return; - addErrorMessage(Message.Diagnostic.from(fileName, token, message), key, autofixes); - } - - void addErrorMessage(const Token[] tokens, string key, string message, AutoFix[] autofixes = null) - { - if (noLint.containsCheck(key)) - return; - addErrorMessage(Message.Diagnostic.from(fileName, tokens, message), key, autofixes); - } - - void addErrorMessage(size_t[2] index, size_t line, size_t[2] columns, string key, string message, AutoFix[] autofixes = null) + extern (D) void addErrorMessage(size_t line, size_t column, string key, string message, AutoFix[] autofixes) { - if (noLint.containsCheck(key)) - return; - addErrorMessage(index, [line, line], columns, key, message, autofixes); + _messages.insert(Message(fileName, line, column, key, message, getName(), autofixes)); } - void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message, AutoFix[] autofixes = null) + extern (D) void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message) { - if (noLint.containsCheck(key)) - return; - auto d = Message.Diagnostic.from(fileName, index, lines, columns, message); - _messages.insert(Message(d, key, getName(), autofixes)); + auto diag = Message.Diagnostic.from(fileName, index, lines, columns, message); + _messages.insert(Message(diag, key, getName())); } - void addErrorMessage(Message.Diagnostic diagnostic, string key, AutoFix[] autofixes = null) + extern (D) void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, + string key, string message, AutoFix[] autofixes) { - if (noLint.containsCheck(key)) - return; - _messages.insert(Message(diagnostic, key, getName(), autofixes)); + auto diag = Message.Diagnostic.from(fileName, index, lines, columns, message); + _messages.insert(Message(diag, key, getName(), autofixes)); } - void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key, AutoFix[] autofixes = null) - { - if (noLint.containsCheck(key)) - return; - _messages.insert(Message(diagnostic, supplemental, key, getName(), autofixes)); - } + extern (D) bool skipTests; /** * The file name */ - string fileName; - - const(Scope)* sc; - - MessageSet _messages; -} - -/// Find the token with the given type, otherwise returns the whole range or a user-specified fallback, if set. -const(Token)[] findTokenForDisplay(const BaseNode node, IdType type, const(Token)[] fallback = null) -{ - return node.tokens.findTokenForDisplay(type, fallback); -} -/// ditto -const(Token)[] findTokenForDisplay(const Token[] tokens, IdType type, const(Token)[] fallback = null) -{ - foreach (i, token; tokens) - if (token.type == type) - return tokens[i .. i + 1]; - return fallback is null ? tokens : fallback; -} - -abstract class ScopedBaseAnalyzer : BaseAnalyzer -{ -public: - this(BaseAnalyzerArguments args) - { - super(args); - } - - - template ScopedVisit(NodeType) - { - override void visit(const NodeType n) - { - pushScopeImpl(); - scope (exit) - popScopeImpl(); - n.accept(this); - } - } - - alias visit = BaseAnalyzer.visit; - - mixin ScopedVisit!BlockStatement; - mixin ScopedVisit!ForeachStatement; - mixin ScopedVisit!ForStatement; - mixin ScopedVisit!Module; - mixin ScopedVisit!StructBody; - mixin ScopedVisit!TemplateDeclaration; - mixin ScopedVisit!WithStatement; - mixin ScopedVisit!WhileStatement; - mixin ScopedVisit!DoStatement; - // mixin ScopedVisit!SpecifiedFunctionBody; // covered by BlockStatement - mixin ScopedVisit!ShortenedFunctionBody; - - override void visit(const SwitchStatement switchStatement) - { - switchStack.length++; - scope (exit) - switchStack.length--; - switchStatement.accept(this); - } - - override void visit(const IfStatement ifStatement) - { - pushScopeImpl(); - if (ifStatement.condition) - ifStatement.condition.accept(this); - if (ifStatement.thenStatement) - ifStatement.thenStatement.accept(this); - popScopeImpl(); - - if (ifStatement.elseStatement) - { - pushScopeImpl(); - ifStatement.elseStatement.accept(this); - popScopeImpl(); - } - } - - static foreach (T; AliasSeq!(CaseStatement, DefaultStatement, CaseRangeStatement)) - override void visit(const T stmt) - { - // case and default statements always open new scopes and close - // previous case scopes - bool close = switchStack.length && switchStack[$ - 1].inCase; - bool b = switchStack[$ - 1].inCase; - switchStack[$ - 1].inCase = true; - scope (exit) - switchStack[$ - 1].inCase = b; - if (close) - { - popScope(); - pushScope(); - stmt.accept(this); - } - else - { - pushScope(); - stmt.accept(this); - popScope(); - } - } - -protected: - /// Called on new scopes, which includes for example: - /// - /// - `module m; /* here, entire file */` - /// - `{ /* here */ }` - /// - `if () { /* here */ } else { /* here */ }` - /// - `foreach (...) { /* here */ }` - /// - `case 1: /* here */ break;` - /// - `case 1: /* here, up to next case */ goto case; case 2: /* here 2 */ break;` - /// - `default: /* here */ break;` - /// - `struct S { /* here */ }` - /// - /// But doesn't include: - /// - /// - `static if (x) { /* not a separate scope */ }` (use `mixin ScopedVisit!ConditionalDeclaration;`) - /// - /// You can `mixin ScopedVisit!NodeType` to automatically call push/popScope - /// on occurences of that NodeType. - abstract void pushScope(); - /// ditto - abstract void popScope(); - - void pushScopeImpl() - { - if (switchStack.length) - switchStack[$ - 1].scopeDepth++; - pushScope(); - } - - void popScopeImpl() - { - if (switchStack.length) - switchStack[$ - 1].scopeDepth--; - popScope(); - } - - struct SwitchStack - { - int scopeDepth; - bool inCase; - } + extern (D) string fileName; - SwitchStack[] switchStack; -} + extern (D) MessageSet _messages; -unittest -{ - import core.exception : AssertError; - import dparse.lexer : getTokensForParser, LexerConfig, StringCache; - import dparse.parser : parseModule; - import dparse.rollback_allocator : RollbackAllocator; - import std.conv : to; - import std.exception : assertThrown; - - // test where we can: - // call `depth(1);` to check that the scope depth is at 1 - // if calls are syntactically not valid, define `auto depth = 1;` - // - // call `isNewScope();` to check that the scope hasn't been checked with isNewScope before - // if calls are syntactically not valid, define `auto isNewScope = void;` - // - // call `isOldScope();` to check that the scope has already been checked with isNewScope - // if calls are syntactically not valid, define `auto isOldScope = void;` - - class TestScopedAnalyzer : ScopedBaseAnalyzer + extern (D) bool shouldIgnoreDecl(UserAttributeDeclaration userAtt, string key) { - this(size_t codeLine) - { - super(BaseAnalyzerArguments("stdin")); + import std.algorithm : startsWith; + import std.string : indexOf; - this.codeLine = codeLine; - } + if (userAtt is null) + return false; - override void visit(const FunctionCallExpression f) + auto atts = userAtt.atts; + if (atts !is null && (*(atts)).length > 0) { - int depth = cast(int) stack.length; - if (f.unaryExpression && f.unaryExpression.primaryExpression - && f.unaryExpression.primaryExpression.identifierOrTemplateInstance) - { - auto fname = f.unaryExpression.primaryExpression.identifierOrTemplateInstance.identifier.text; - if (fname == "depth") - { - assert(f.arguments.tokens.length == 3); - auto expected = f.arguments.tokens[1].text.to!int; - assert(expected == depth, "Expected depth=" - ~ expected.to!string ~ " in line " ~ (codeLine + f.tokens[0].line).to!string - ~ ", but got depth=" ~ depth.to!string); - } - else if (fname == "isNewScope") - { - assert(!stack[$ - 1]); - stack[$ - 1] = true; - } - else if (fname == "isOldScope") - { - assert(stack[$ - 1]); - } - } - } - - override void visit(const AutoDeclarationPart p) - { - int depth = cast(int) stack.length; - - if (p.identifier.text == "depth") - { - assert(p.initializer.tokens.length == 1); - auto expected = p.initializer.tokens[0].text.to!int; - assert(expected == depth, "Expected depth=" - ~ expected.to!string ~ " in line " ~ (codeLine + p.tokens[0].line).to!string - ~ ", but got depth=" ~ depth.to!string); - } - else if (p.identifier.text == "isNewScope") - { - assert(!stack[$ - 1]); - stack[$ - 1] = true; - } - else if (p.identifier.text == "isOldScope") + if (auto att = (*(atts))[0].isStringExp()) { - assert(stack[$ - 1]); + string attStr = cast(string) att.toStringz(); + if (attStr.startsWith("nolint") && attStr.indexOf(key) > 0) + return true; } } - override void pushScope() - { - stack.length++; - } - - override void popScope() - { - stack.length--; - } - - alias visit = ScopedBaseAnalyzer.visit; - - bool[] stack; - size_t codeLine; - } - - void testScopes(string code, size_t codeLine = __LINE__ - 1) - { - StringCache cache = StringCache(4096); - LexerConfig config; - RollbackAllocator rba; - auto tokens = getTokensForParser(code, config, &cache); - Module m = parseModule(tokens, "stdin", &rba); - - auto analyzer = new TestScopedAnalyzer(codeLine); - analyzer.visit(m); + return false; } - - testScopes(q{ - auto isNewScope = void; - auto depth = 1; - auto isOldScope = void; - }); - - assertThrown!AssertError(testScopes(q{ - auto isNewScope = void; - auto isNewScope = void; - })); - - assertThrown!AssertError(testScopes(q{ - auto isOldScope = void; - })); - - assertThrown!AssertError(testScopes(q{ - auto depth = 2; - })); - - testScopes(q{ - auto isNewScope = void; - auto depth = 1; - - void foo() { - isNewScope(); - isOldScope(); - depth(2); - switch (a) - { - case 1: - isNewScope(); - depth(4); - break; - depth(4); - isOldScope(); - case 2: - isNewScope(); - depth(4); - if (a) - { - isNewScope(); - depth(6); - default: - isNewScope(); - depth(6); // since cases/default opens new scope - break; - case 3: - isNewScope(); - depth(6); // since cases/default opens new scope - break; - default: - isNewScope(); - depth(6); // since cases/default opens new scope - break; - } - break; - depth(4); - default: - isNewScope(); - depth(4); - break; - depth(4); - } - - isOldScope(); - depth(2); - - switch (a) - { - isNewScope(); - depth(3); - isOldScope(); - default: - isNewScope(); - depth(4); - break; - isOldScope(); - case 1: - isNewScope(); - depth(4); - break; - isOldScope(); - } - } - - auto isOldScope = void; - }); } diff --git a/src/dscanner/analysis/body_on_disabled_funcs.d b/src/dscanner/analysis/body_on_disabled_funcs.d index c6476a84..c6183108 100644 --- a/src/dscanner/analysis/body_on_disabled_funcs.d +++ b/src/dscanner/analysis/body_on_disabled_funcs.d @@ -1,143 +1,76 @@ module dscanner.analysis.body_on_disabled_funcs; import dscanner.analysis.base; -import dparse.ast; -import dparse.lexer; -import dsymbol.scope_; -import std.meta : AliasSeq; +import dmd.astenums : STC; -final class BodyOnDisabledFuncsCheck : BaseAnalyzer +extern (C++) class BodyOnDisabledFuncsCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"body_on_disabled_func_check"; - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum string KEY = "dscanner.confusing.disabled_function_with_body"; + private enum FUNC_MSG = "Function marked with '@disabled' should not have a body"; + private enum CTOR_MSG = "Constructor marked with '@disabled' should not have a body"; + private enum DTOR_MSG = "Destructor marked with '@disabled' should not have a body"; - static foreach (AggregateType; AliasSeq!(InterfaceDeclaration, ClassDeclaration, - StructDeclaration, UnionDeclaration, FunctionDeclaration)) - override void visit(const AggregateType t) - { - scope wasDisabled = isDisabled; - isDisabled = false; - t.accept(this); - isDisabled = wasDisabled; - } + private bool isDisabled; - override void visit(const Declaration dec) + extern (D) this(string fileName, bool skipTests = false) { - foreach (attr; dec.attributes) - { - if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable") { - // found attr block w. disable: dec.constructor - scope wasDisabled = isDisabled; - isDisabled = true; - visitDeclarationInner(dec); - dec.accept(this); - isDisabled = wasDisabled; - return; - } - } + super(fileName, skipTests); + } - visitDeclarationInner(dec); - scope wasDisabled = isDisabled; - dec.accept(this); + override void visit(AST.StorageClassDeclaration storageClassDecl) + { + bool wasDisabled = isDisabled; + isDisabled = (storageClassDecl.stc & STC.disable) != 0; + super.visit(storageClassDecl); isDisabled = wasDisabled; } -private: - bool isDisabled = false; + mixin VisitAggregate!(AST.ClassDeclaration); + mixin VisitAggregate!(AST.InterfaceDeclaration); + mixin VisitAggregate!(AST.StructDeclaration); + mixin VisitAggregate!(AST.UnionDeclaration); - bool isDeclDisabled(T)(const T dec) + private template VisitAggregate(NodeType) { - // `@disable { ... }` - if (isDisabled) - return true; - - static if (__traits(hasMember, T, "storageClasses")) + override void visit(NodeType node) { - // `@disable doThing() {}` - if (hasDisabledStorageclass(dec.storageClasses)) - return true; - } - - // `void doThing() @disable {}` - return hasDisabledMemberFunctionAttribute(dec.memberFunctionAttributes); - } + if (isDisabled || (node.storage_class & STC.disable) != 0) + return; - void visitDeclarationInner(const Declaration dec) - { - if (dec.attributeDeclaration !is null - && dec.attributeDeclaration.attribute - && dec.attributeDeclaration.attribute.atAttribute - && dec.attributeDeclaration.attribute.atAttribute.identifier.text == "disable") - { - // found `@disable:`, so all code in this block is now disabled - isDisabled = true; - } - else if (dec.functionDeclaration !is null - && isDeclDisabled(dec.functionDeclaration) - && dec.functionDeclaration.functionBody !is null - && dec.functionDeclaration.functionBody.missingFunctionBody is null) - { - addErrorMessage(dec.functionDeclaration.functionBody, - KEY, "Function marked with '@disabled' should not have a body"); - } - else if (dec.constructor !is null - && isDeclDisabled(dec.constructor) - && dec.constructor.functionBody !is null - && dec.constructor.functionBody.missingFunctionBody is null) - { - addErrorMessage(dec.constructor.functionBody, - KEY, "Constructor marked with '@disabled' should not have a body"); - } - else if (dec.destructor !is null - && isDeclDisabled(dec.destructor) - && dec.destructor.functionBody !is null - && dec.destructor.functionBody.missingFunctionBody is null) - { - addErrorMessage(dec.destructor.functionBody, - KEY, "Destructor marked with '@disabled' should not have a body"); + bool wasDisabled = isDisabled; + isDisabled = false; + super.visit(node); + isDisabled = wasDisabled; } } - bool hasDisabledStorageclass(const(StorageClass[]) storageClasses) - { - foreach (sc; storageClasses) - { - if (sc.atAttribute !is null && sc.atAttribute.identifier.text == "disable") - return true; - } - return false; - } + mixin VisitFunction!(AST.FuncDeclaration, FUNC_MSG); + mixin VisitFunction!(AST.CtorDeclaration, CTOR_MSG); + mixin VisitFunction!(AST.DtorDeclaration, DTOR_MSG); - bool hasDisabledMemberFunctionAttribute(const(MemberFunctionAttribute[]) memberFunctionAttributes) + private template VisitFunction(NodeType, string MSG) { - foreach (attr; memberFunctionAttributes) + override void visit(NodeType node) { - if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable") - return true; + if ((isDisabled || (node.storage_class & STC.disable) != 0) && node.fbody !is null) + addErrorMessage(cast(ulong) node.loc.linnum, cast(ulong) node.loc.charnum, KEY, MSG); } - return false; } - - enum string KEY = "dscanner.confusing.disabled_function_with_body"; } unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.body_on_disabled_func_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class C1 { this() {} @@ -159,12 +92,9 @@ unittest } } - this() {} /+ - ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ - void doThing() {} /+ - ^^ [warn]: Function marked with '@disabled' should not have a body +/ - ~this() {} /+ - ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ + this() {} // [warn]: Constructor marked with '@disabled' should not have a body + void doThing() {} // [warn]: Function marked with '@disabled' should not have a body + ~this() {} // [warn]: Destructor marked with '@disabled' should not have a body this(); void doThing(); @@ -173,28 +103,18 @@ unittest class C2 { - @disable this() {} /+ - ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ - @disable { this() {} } /+ - ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ - this() @disable {} /+ - ^^ [warn]: Constructor marked with '@disabled' should not have a body +/ - - @disable void doThing() {} /+ - ^^ [warn]: Function marked with '@disabled' should not have a body +/ - @disable doThing() {} /+ - ^^ [warn]: Function marked with '@disabled' should not have a body +/ - @disable { void doThing() {} } /+ - ^^ [warn]: Function marked with '@disabled' should not have a body +/ - void doThing() @disable {} /+ - ^^ [warn]: Function marked with '@disabled' should not have a body +/ - - @disable ~this() {} /+ - ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ - @disable { ~this() {} } /+ - ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ - ~this() @disable {} /+ - ^^ [warn]: Destructor marked with '@disabled' should not have a body +/ + @disable this() {} // [warn]: Constructor marked with '@disabled' should not have a body + @disable { this() {} } // [warn]: Constructor marked with '@disabled' should not have a body + this() @disable {} // [warn]: Constructor marked with '@disabled' should not have a body + + @disable void doThing() {} // [warn]: Function marked with '@disabled' should not have a body + @disable doThing() {} // [warn]: Function marked with '@disabled' should not have a body + @disable { void doThing() {} } // [warn]: Function marked with '@disabled' should not have a body + void doThing() @disable {} // [warn]: Function marked with '@disabled' should not have a body + + @disable ~this() {} // [warn]: Destructor marked with '@disabled' should not have a body + @disable { ~this() {} } // [warn]: Destructor marked with '@disabled' should not have a body + ~this() @disable {} // [warn]: Destructor marked with '@disabled' should not have a body @disable this(); @disable { this(); } diff --git a/src/dscanner/analysis/builtin_property_names.d b/src/dscanner/analysis/builtin_property_names.d index 45fe4f2c..cc5a69b7 100644 --- a/src/dscanner/analysis/builtin_property_names.d +++ b/src/dscanner/analysis/builtin_property_names.d @@ -5,14 +5,7 @@ module dscanner.analysis.builtin_property_names; -import std.stdio; -import std.regex; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_; -import std.algorithm : map; /** * The following code should be killed with fire: @@ -27,63 +20,64 @@ import std.algorithm : map; * } * --- */ -final class BuiltinPropertyNameCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; +extern(C++) class BuiltinPropertyNameCheck(AST) : BaseAnalyzerDmd +{ + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"builtin_property_names_check"; - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); } - override void visit(const FunctionDeclaration fd) - { - if (depth > 0 && isBuiltinProperty(fd.name.text)) - { - addErrorMessage(fd.name, KEY, generateErrorMessage(fd.name.text)); - } - fd.accept(this); - } + mixin AggregateVisit!(AST.StructDeclaration); + mixin AggregateVisit!(AST.ClassDeclaration); + mixin AggregateVisit!(AST.InterfaceDeclaration); + mixin AggregateVisit!(AST.UnionDeclaration); - override void visit(const FunctionBody functionBody) + override void visit(AST.VarDeclaration vd) { - immutable int d = depth; - scope (exit) - depth = d; - depth = 0; - functionBody.accept(this); + if (inAggregate && isBuiltinProperty(vd.ident.toString())) + addErrorMessage(cast(ulong) vd.loc.linnum, cast(ulong) vd.loc.charnum, + KEY, generateErrorMessage(vd.ident.toString())); } - override void visit(const AutoDeclaration ad) + override void visit(AST.FuncDeclaration fd) { - if (depth > 0) - foreach (i; ad.parts.map!(a => a.identifier)) - { - if (isBuiltinProperty(i.text)) - addErrorMessage(i, KEY, generateErrorMessage(i.text)); - } + if (inAggregate && isBuiltinProperty(fd.ident.toString())) + addErrorMessage(cast(ulong) fd.loc.linnum, cast(ulong) fd.loc.charnum, + KEY, generateErrorMessage(fd.ident.toString())); } - override void visit(const Declarator d) + override void visit(AST.AliasDeclaration ad) { - if (depth > 0 && isBuiltinProperty(d.name.text)) - addErrorMessage(d.name, KEY, generateErrorMessage(d.name.text)); + if (inAggregate && isBuiltinProperty(ad.ident.toString())) + addErrorMessage(cast(ulong) ad.loc.linnum, cast(ulong) ad.loc.charnum, + KEY, generateErrorMessage(ad.ident.toString())); } - override void visit(const StructBody sb) + override void visit(AST.TemplateDeclaration td) { - depth++; - sb.accept(this); - depth--; + if (inAggregate && isBuiltinProperty(td.ident.toString())) + addErrorMessage(cast(ulong) td.loc.linnum, cast(ulong) td.loc.charnum, + KEY, generateErrorMessage(td.ident.toString())); } private: - enum string KEY = "dscanner.confusing.builtin_property_names"; - string generateErrorMessage(string name) + template AggregateVisit(NodeType) + { + override void visit(NodeType n) + { + inAggregate++; + super.visit(n); + inAggregate--; + } + } + + extern(D) string generateErrorMessage(const(char)[] name) { import std.string : format; @@ -91,7 +85,7 @@ private: ~ " confuse code that depends on the '.%s' property of a type.", name, name); } - bool isBuiltinProperty(string name) + extern(D) bool isBuiltinProperty(const(char)[] name) { import std.algorithm : canFind; @@ -99,26 +93,25 @@ private: } enum string[] BuiltinProperties = ["init", "sizeof", "mangleof", "alignof", "stringof"]; - int depth; + int inAggregate; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.builtin_property_names_check = Check.enabled; assertAnalyzerWarnings(q{ class SomeClass { - void init(); /+ - ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ - int init; /+ - ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ - auto init = 10; /+ - ^^^^ [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. +/ + void init(); // [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. + int init; // [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. + auto init = 10; // [warn]: Avoid naming members 'init'. This can confuse code that depends on the '.init' property of a type. } }c, sac); - stderr.writeln("Unittest for NumberStyleCheck passed."); + stderr.writeln("Unittest for BuiltinPropertyNamesCheck passed."); } diff --git a/src/dscanner/analysis/comma_expression.d b/src/dscanner/analysis/comma_expression.d deleted file mode 100644 index 551ffd1d..00000000 --- a/src/dscanner/analysis/comma_expression.d +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright Brian Schott (Hackerpilot) 2014. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) - -module dscanner.analysis.comma_expression; - -import dparse.ast; -import dparse.lexer; -import dscanner.analysis.base; -import dsymbol.scope_; - -/** - * Check for uses of the comma expression. - */ -final class CommaExpressionCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - - mixin AnalyzerInfo!"comma_expression_check"; - - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const Expression ex) - { - if (ex.items.length > 1 && interest > 0) - { - addErrorMessage(ex, KEY, "Avoid using the comma expression."); - } - ex.accept(this); - } - - override void visit(const AssignExpression ex) - { - ++interest; - ex.accept(this); - --interest; - } - - // Dconf 2016 - override void visit(const SynchronizedStatement ss) - { - if (ss.expression !is null) - { - ++interest; - visit(ss.expression); - --interest; - } - visit(ss.statementNoCaseNoDefault); - } - - invariant - { - assert(interest >= 0); - } - - int interest; - - private enum string KEY = "dscanner.suspicious.comma_expression"; -} diff --git a/src/dscanner/analysis/constructors.d b/src/dscanner/analysis/constructors.d index c87f36bb..84765d03 100644 --- a/src/dscanner/analysis/constructors.d +++ b/src/dscanner/analysis/constructors.d @@ -1,104 +1,63 @@ module dscanner.analysis.constructors; -import dparse.ast; -import dparse.lexer; import std.stdio; -import std.typecons : Rebindable; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; -final class ConstructorCheck : BaseAnalyzer +extern(C++) class ConstructorCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"constructor_check"; - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const ClassDeclaration classDeclaration) + extern(D) this(string fileName) { - const oldHasDefault = hasDefaultArgConstructor; - const oldHasNoArg = hasNoArgConstructor; - hasNoArgConstructor = null; - hasDefaultArgConstructor = null; - immutable State prev = state; - state = State.inClass; - classDeclaration.accept(this); - if (hasNoArgConstructor && hasDefaultArgConstructor) - { - addErrorMessage( - Message.Diagnostic.from(fileName, classDeclaration.name, - "This class has a zero-argument constructor as well as a" - ~ " constructor with one default argument. This can be confusing."), - [ - Message.Diagnostic.from(fileName, hasNoArgConstructor, "zero-argument constructor defined here"), - Message.Diagnostic.from(fileName, hasDefaultArgConstructor, "default argument constructor defined here") - ], - "dscanner.confusing.constructor_args" - ); - } - hasDefaultArgConstructor = oldHasDefault; - hasNoArgConstructor = oldHasNoArg; - state = prev; + super(fileName); } - override void visit(const StructDeclaration structDeclaration) + override void visit(AST.ClassDeclaration d) { - immutable State prev = state; - state = State.inStruct; - structDeclaration.accept(this); - state = prev; - } + bool hasDefaultArgConstructor = false; + bool hasNoArgConstructor = false; - override void visit(const Constructor constructor) - { - final switch (state) + if (d.members) { - case State.inStruct: - if (constructor.parameters.parameters.length == 1 - && constructor.parameters.parameters[0].default_ !is null) - { - const(Token)[] tokens = constructor.parameters.parameters[0].default_.tokens; - assert(tokens.length); - // we extend the token range to the `=` sign, since it's continuous - tokens = (tokens.ptr - 1)[0 .. tokens.length + 1]; - addErrorMessage(tokens, - "dscanner.confusing.struct_constructor_default_args", - "This struct constructor can never be called with its " - ~ "default argument."); - } - break; - case State.inClass: - if (constructor.parameters.parameters.length == 1 - && constructor.parameters.parameters[0].default_ !is null) + foreach (s; *d.members) { - hasDefaultArgConstructor = constructor; - } - else if (constructor.parameters.parameters.length == 0) - hasNoArgConstructor = constructor; - break; - case State.ignoring: - break; - } - } + if (auto cd = s.isCtorDeclaration()) + { + auto tf = cd.type.isTypeFunction(); -private: + if (tf) + { + if (tf.parameterList.parameters.length == 0) + hasNoArgConstructor = true; + else + { + // Check if all parameters have a default value + hasDefaultArgConstructor = true; - enum State : ubyte - { - ignoring, - inClass, - inStruct - } + foreach (param; *tf.parameterList.parameters) + if (param.defaultArg is null) + hasDefaultArgConstructor = false; + } + } + } - State state; + s.accept(this); + } + } - Rebindable!(const Constructor) hasNoArgConstructor; - Rebindable!(const Constructor) hasDefaultArgConstructor; + if (hasNoArgConstructor && hasDefaultArgConstructor) + { + addErrorMessage(cast(ulong) d.loc.linnum, + cast(ulong) d.loc.charnum, KEY, MESSAGE); + } + } + +private: + enum MESSAGE = "This class has a zero-argument constructor as well as a" + ~ " constructor with default arguments. This can be confusing."; + enum KEY = "dscanner.confusing.constructor_args"; } unittest @@ -107,20 +66,23 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.constructor_check = Check.enabled; - // TODO: test supplemental diagnostics - assertAnalyzerWarnings(q{ - class Cat /+ - ^^^ [warn]: This class has a zero-argument constructor as well as a constructor with one default argument. This can be confusing. +/ + assertAnalyzerWarningsDMD(q{ + class Cat // [warn]: This class has a zero-argument constructor as well as a constructor with default arguments. This can be confusing. { this() {} this(string name = "kittie") {} } - struct Dog + class Cat // [warn]: This class has a zero-argument constructor as well as a constructor with default arguments. This can be confusing. + { + this() {} + this(string name = "kittie", int x = 2) {} + } + + class Cat { this() {} - this(string name = "doggie") {} /+ - ^^^^^^^^^^ [warn]: This struct constructor can never be called with its default argument. +/ + this(string name = "kittie", int x) {} } }c, sac); diff --git a/src/dscanner/analysis/cyclomatic_complexity.d b/src/dscanner/analysis/cyclomatic_complexity.d index 27e6d2dc..f6013682 100644 --- a/src/dscanner/analysis/cyclomatic_complexity.d +++ b/src/dscanner/analysis/cyclomatic_complexity.d @@ -4,12 +4,8 @@ module dscanner.analysis.cyclomatic_complexity; -import dparse.ast; -import dparse.lexer; -import dsymbol.scope_ : Scope; import dscanner.analysis.base; -import dscanner.analysis.helpers; - +import dmd.location : Loc; import std.format; /// Implements a basic cyclomatic complexity algorithm using the AST. @@ -35,12 +31,9 @@ import std.format; /// See: https://en.wikipedia.org/wiki/Cyclomatic_complexity /// Rules based on http://cyvis.sourceforge.net/cyclomatic_complexity.html /// and https://github.com/fzipp/gocyclo -final class CyclomaticComplexityCheck : BaseAnalyzer +extern (C++) class CyclomaticComplexityCheck(AST) : BaseAnalyzerDmd { - /// Message key emitted when the threshold is reached - enum string KEY = "dscanner.metric.cyclomatic_complexity"; - /// Human readable message emitted when the threshold is reached - enum string MESSAGE = "Cyclomatic complexity of this function is %s."; + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"cyclomatic_complexity"; /// Maximum cyclomatic complexity. Once the cyclomatic complexity is greater @@ -50,52 +43,44 @@ final class CyclomaticComplexityCheck : BaseAnalyzer /// unmaintainable / untestable. /// /// For clean development a threshold like 20 can be used instead. - int maxCyclomaticComplexity; + immutable int maxCyclomaticComplexity; - /// - this(BaseAnalyzerArguments args, int maxCyclomaticComplexity = 50) + private enum string KEY = "dscanner.metric.cyclomatic_complexity"; + private enum string MESSAGE = "Cyclomatic complexity of this function is %s."; + + private int[] complexityStack = [0]; + private bool[] inLoop = [false]; + + extern (D) this(string fileName, bool skipTests = false, int maxCyclomaticComplexity = 50) { - super(args); + super(fileName, skipTests); this.maxCyclomaticComplexity = maxCyclomaticComplexity; } - mixin VisitComplex!IfStatement; - mixin VisitComplex!CaseStatement; - mixin VisitComplex!CaseRangeStatement; - mixin VisitLoop!DoStatement; - mixin VisitLoop!WhileStatement; - mixin VisitLoop!ForStatement; - mixin VisitLoop!ForeachStatement; - mixin VisitComplex!AndAndExpression; - mixin VisitComplex!OrOrExpression; - mixin VisitComplex!TernaryExpression; - mixin VisitComplex!ThrowExpression; - mixin VisitComplex!Catch; - mixin VisitComplex!LastCatch; - mixin VisitComplex!ReturnStatement; - mixin VisitComplex!FunctionLiteralExpression; - mixin VisitComplex!GotoStatement; - mixin VisitComplex!ContinueStatement; - - override void visit(const SwitchStatement n) + override void visit(AST.TemplateDeclaration templateDecl) { - inLoop.assumeSafeAppend ~= false; - scope (exit) - inLoop.length--; - n.accept(this); + foreach (member; *templateDecl.members) + member.accept(this); } - override void visit(const BreakStatement b) + override void visit(AST.FuncDeclaration funDecl) { - if (b.label !is Token.init || inLoop[$ - 1]) - complexityStack[$ - 1]++; + if (funDecl.fbody is null) + return; + + analyzeFunctionBody(funDecl.fbody, funDecl.loc); } - override void visit(const FunctionDeclaration fun) + override void visit(AST.UnitTestDeclaration unitTestDecl) { - if (!fun.functionBody) + if (skipTests) return; + analyzeFunctionBody(unitTestDecl.fbody, unitTestDecl.loc); + } + + private void analyzeFunctionBody(AST.Statement functionBody, Loc location) + { complexityStack.assumeSafeAppend ~= 1; inLoop.assumeSafeAppend ~= false; scope (exit) @@ -103,58 +88,100 @@ final class CyclomaticComplexityCheck : BaseAnalyzer complexityStack.length--; inLoop.length--; } - fun.functionBody.accept(this); - testComplexity(fun.name); + + functionBody.accept(this); + testComplexity(location.linnum, location.charnum); } - override void visit(const Unittest unittest_) + private void testComplexity(size_t line, size_t column) { - if (!skipTests) - { - complexityStack.assumeSafeAppend ~= 1; - inLoop.assumeSafeAppend ~= false; - scope (exit) - { - complexityStack.length--; - inLoop.length--; - } - unittest_.accept(this); - testComplexity(unittest_.findTokenForDisplay(tok!"unittest")); - } + auto complexity = complexityStack[$ - 1]; + if (complexity > maxCyclomaticComplexity) + addErrorMessage(line, column, KEY, format!MESSAGE(complexity)); } - alias visit = BaseAnalyzer.visit; -private: - int[] complexityStack = [0]; - bool[] inLoop = [false]; + override void visit(AST.FuncExp funcExp) + { + if (funcExp.fd is null) + return; + + complexityStack[$ - 1]++; + funcExp.fd.accept(this); + } - void testComplexity(T)(T annotatable) + mixin VisitComplex!(AST.IfStatement); + mixin VisitComplex!(AST.LogicalExp); + mixin VisitComplex!(AST.CondExp); + mixin VisitComplex!(AST.CaseStatement); + mixin VisitComplex!(AST.CaseRangeStatement); + mixin VisitComplex!(AST.ReturnStatement); + mixin VisitComplex!(AST.ContinueStatement); + mixin VisitComplex!(AST.GotoStatement); + mixin VisitComplex!(AST.TryFinallyStatement); + mixin VisitComplex!(AST.ThrowExp); + + private template VisitComplex(NodeType, int increase = 1) { - auto complexity = complexityStack[$ - 1]; - if (complexity > maxCyclomaticComplexity) + override void visit(NodeType nodeType) { - addErrorMessage(annotatable, KEY, format!MESSAGE(complexity)); + complexityStack[$ - 1] += increase; + super.visit(nodeType); } } - template VisitComplex(NodeType, int increase = 1) + override void visit(AST.SwitchStatement switchStatement) { - override void visit(const NodeType n) + inLoop.assumeSafeAppend ~= false; + scope (exit) + inLoop.length--; + + switchStatement.condition.accept(this); + switchStatement._body.accept(this); + } + + override void visit(AST.BreakStatement breakStatement) + { + if (inLoop[$ - 1]) + complexityStack[$ - 1]++; + } + + override void visit(AST.TryCatchStatement tryCatchStatement) + { + tryCatchStatement._body.accept(this); + + if (tryCatchStatement.catches !is null) { - complexityStack[$ - 1] += increase; - n.accept(this); + foreach (catchStatement; *(tryCatchStatement.catches)) + { + complexityStack[$ - 1]++; + catchStatement.handler.accept(this); + } } } - template VisitLoop(NodeType, int increase = 1) + override void visit(AST.StaticForeachStatement staticForeachStatement) { - override void visit(const NodeType n) + // StaticForeachStatement visit has to be overridden in order to avoid visiting + // its forEachStatement member, which would increase the complexity. + return; + } + + mixin VisitLoop!(AST.DoStatement); + mixin VisitLoop!(AST.WhileStatement); + mixin VisitLoop!(AST.ForStatement); + mixin VisitLoop!(AST.ForeachRangeStatement); + mixin VisitLoop!(AST.ForeachStatement); + + private template VisitLoop(NodeType, int increase = 1) + { + override void visit(NodeType nodeType) { inLoop.assumeSafeAppend ~= true; scope (exit) inLoop.length--; + complexityStack[$ - 1] += increase; - n.accept(this); + super.visit(nodeType); } } } @@ -162,34 +189,34 @@ private: unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.cyclomatic_complexity = Check.enabled; sac.max_cyclomatic_complexity = 0; - assertAnalyzerWarnings(q{ -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/ + + // TODO: Remove redundant tests and break down remaining tests in individual assertions + assertAnalyzerWarningsDMD(q{ +unittest // [warn]: Cyclomatic complexity of this function is 1. { } -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/ +// unit test +unittest // [warn]: Cyclomatic complexity of this function is 1. { writeln("hello"); writeln("world"); } -void main(string[] args) /+ - ^^^^ [warn]: Cyclomatic complexity of this function is 3. +/ +void main(string[] args) // [warn]: Cyclomatic complexity of this function is 3. { if (!args.length) return; writeln("hello ", args); } -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 1. +/ +unittest // [warn]: Cyclomatic complexity of this function is 1. { // static if / static foreach does not increase cyclomatic complexity static if (stuff) @@ -197,8 +224,7 @@ unittest /+ int a; } -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 2. +/ +unittest // [warn]: Cyclomatic complexity of this function is 2. { foreach (i; 0 .. 2) { @@ -206,8 +232,7 @@ unittest /+ int a; } -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 3. +/ +unittest // [warn]: Cyclomatic complexity of this function is 3. { foreach (i; 0 .. 2) { @@ -216,8 +241,7 @@ unittest /+ int a; } -unittest /+ -^^^^^^^^ [warn]: Cyclomatic complexity of this function is 2. +/ +unittest // [warn]: Cyclomatic complexity of this function is 2. { switch (x) { @@ -229,8 +253,8 @@ unittest /+ int a; } -bool shouldRun(check : BaseAnalyzer)( /+ - ^^^^^^^^^ [warn]: Cyclomatic complexity of this function is 20. +/ +// Template, other (tested) stuff +bool shouldRun(check : BaseAnalyzer)( // [warn]: Cyclomatic complexity of this function is 20. string moduleName, const ref StaticAnalysisConfig config) { enum string a = check.name; @@ -262,7 +286,157 @@ bool shouldRun(check : BaseAnalyzer)( /+ // by default: include all modules return true; } - }c, sac); + + assertAnalyzerWarningsDMD(q{ + // goto, return + void returnGoto() // [warn]: Cyclomatic complexity of this function is 3. + { + goto hello; + int a = 0; + a += 9; + + hello: + return; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // if, else, ternary operator + void ifElseTernary() // [warn]: Cyclomatic complexity of this function is 4. + { + if (1 > 2) + { + int a; + } + else if (2 > 1) + { + int b; + } + else + { + int c; + } + + int d = true ? 1 : 2; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // static if and static foreach don't increase cyclomatic complexity + void staticIfFor() // [warn]: Cyclomatic complexity of this function is 1. + { + static if (stuff) + int a; + + int b; + + static foreach(i; 0 .. 10) + { + pragma(msg, i); + } + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // function literal (lambda) + void lambda() // [warn]: Cyclomatic complexity of this function is 2. + { + auto x = (int a) => a + 1; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // loops: for, foreach, while, do - while + void controlFlow() // [warn]: Cyclomatic complexity of this function is 7. + { + int x = 0; + + for (int i = 0; i < 100; i++) + { + i++; + } + + foreach (i; 0 .. 2) + { + x += i; + continue; + } + + while (true) + { + break; + } + + do + { + int x = 0; + } while (true); + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // switch - case + void switchCaseCaseRange() // [warn]: Cyclomatic complexity of this function is 5. + { + switch (x) + { + case 1: + break; + case 2: + case 3: + break; + case 7: .. case 10: + break; + default: + break; + } + int a; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // if, else, logical expressions + void ifConditions() // [warn]: Cyclomatic complexity of this function is 5. + { + if (true && false) + { + doX(); + } + else if (true || false) + { + doY(); + } + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + // catch, throw + void throwCatch() // [warn]: Cyclomatic complexity of this function is 5. + { + int x; + try + { + x = 5; + } + catch (Exception e) + { + x = 7; + } + catch (Exception a) + { + x = 8; + } + catch (Exception x) + { + throw new Exception("Exception"); + } + finally + { + x = 9; + } + } + }c, sac); + stderr.writeln("Unittest for CyclomaticComplexityCheck passed."); } diff --git a/src/dscanner/analysis/del.d b/src/dscanner/analysis/del.d index a97de00d..1749cd9d 100644 --- a/src/dscanner/analysis/del.d +++ b/src/dscanner/analysis/del.d @@ -5,77 +5,80 @@ module dscanner.analysis.del; -import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dsymbol.scope_; /** * Checks for use of the deprecated 'delete' keyword */ -final class DeleteCheck : BaseAnalyzer +extern(C++) class DeleteCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"delete_check"; - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum KEY = "dscanner.deprecated.delete_keyword"; + private enum MSG = "Avoid using the 'delete' keyword."; - override void visit(const DeleteExpression d) + extern(D) this(string fileName) { - addErrorMessage(d.tokens[0], KEY, - "Avoid using the 'delete' keyword.", - [AutoFix.replacement(d.tokens[0], `destroy(`, "Replace delete with destroy()") - .concat(AutoFix.insertionAfter(d.tokens[$ - 1], ")"))]); - d.accept(this); + super(fileName); } - private enum string KEY = "dscanner.deprecated.delete_keyword"; -} - -unittest -{ - import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; - - StaticAnalysisConfig sac = disabledConfig(); - sac.delete_check = Check.enabled; - assertAnalyzerWarnings(q{ - void testDelete() - { - int[int] data = [1 : 2]; - delete data[1]; /+ - ^^^^^^ [warn]: Avoid using the 'delete' keyword. +/ - - auto a = new Class(); - delete a; /+ - ^^^^^^ [warn]: Avoid using the 'delete' keyword. +/ - } - }c, sac); - - assertAutoFix(q{ - void testDelete() - { - int[int] data = [1 : 2]; - delete data[1]; // fix + override void visit(AST.DeleteExp d) + { + import dmd.hdrgen : toChars; + import std.conv : to; - auto a = new Class(); - delete a; // fix - } - }c, q{ - void testDelete() - { - int[int] data = [1 : 2]; - destroy(data[1]); // fix + string exprStr = to!string(toChars(d)); - auto a = new Class(); - destroy(a); // fix - } - }c, sac); + addErrorMessage( + cast(ulong) d.loc.linnum, cast(ulong) d.loc.charnum, KEY, MSG, + [AutoFix.replacement(d.loc.fileOffset, d.loc.fileOffset + 6, `destroy(`, "Replace delete with destroy()") + .concat(AutoFix.insertionAt(d.loc.fileOffset + exprStr.length, ")"))] + ); - stderr.writeln("Unittest for DeleteCheck passed."); + super.visit(d); + } } + +//unittest +//{ +// import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; +// import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; +// import std.stdio : stderr; +// +// StaticAnalysisConfig sac = disabledConfig(); +// sac.delete_check = Check.enabled; +// +// assertAnalyzerWarningsDMD(q{ +// void testDelete() +// { +// int[int] data = [1 : 2]; +// delete data[1]; // [warn]: Avoid using the 'delete' keyword. +// +// auto a = new Class(); +// delete a; // [warn]: Avoid using the 'delete' keyword. +// } +// }c, sac); +// +// assertAutoFix(q{ +// void testDelete() +// { +// int[int] data = [1 : 2]; +// delete data[1]; // fix +// +// auto a = new Class(); +// delete a; // fix +// } +// }c, q{ +// void testDelete() +// { +// int[int] data = [1 : 2]; +// destroy(data[1]); // fix +// +// auto a = new Class(); +// destroy(a); // fix +// } +// }c, sac); +// +// stderr.writeln("Unittest for DeleteCheck passed."); +//} diff --git a/src/dscanner/analysis/duplicate_attribute.d b/src/dscanner/analysis/duplicate_attribute.d deleted file mode 100644 index 647e576a..00000000 --- a/src/dscanner/analysis/duplicate_attribute.d +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2014, Matthew Brennan Jones -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) - -module dscanner.analysis.duplicate_attribute; - -import std.stdio; -import std.string; -import dparse.ast; -import dparse.lexer; -import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; - -/** - * Checks for duplicate attributes such as @property, @safe, - * @trusted, @system, pure, and nothrow - */ -final class DuplicateAttributeCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - - mixin AnalyzerInfo!"duplicate_attribute"; - - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const Declaration node) - { - checkAttributes(node); - node.accept(this); - } - - void checkAttributes(const Declaration node) - { - bool hasProperty; - bool hasSafe; - bool hasTrusted; - bool hasSystem; - bool hasPure; - bool hasNoThrow; - - // Check the attributes - foreach (attribute; node.attributes) - { - const(Token)[] tokens; - string attributeName = getAttributeName(attribute, tokens); - if (!attributeName || !tokens.length) - return; - - // Check for the attributes - checkDuplicateAttribute(attributeName, "property", tokens, hasProperty); - checkDuplicateAttribute(attributeName, "safe", tokens, hasSafe); - checkDuplicateAttribute(attributeName, "trusted", tokens, hasTrusted); - checkDuplicateAttribute(attributeName, "system", tokens, hasSystem); - checkDuplicateAttribute(attributeName, "pure", tokens, hasPure); - checkDuplicateAttribute(attributeName, "nothrow", tokens, hasNoThrow); - } - - // Just return if missing function nodes - if (!node.functionDeclaration || !node.functionDeclaration.memberFunctionAttributes) - return; - - // Check the functions - foreach (memberFunctionAttribute; node.functionDeclaration.memberFunctionAttributes) - { - const(Token)[] tokens; - string attributeName = getAttributeName(memberFunctionAttribute, tokens); - if (!attributeName || !tokens.length) - return; - - // Check for the attributes - checkDuplicateAttribute(attributeName, "property", tokens, hasProperty); - checkDuplicateAttribute(attributeName, "safe", tokens, hasSafe); - checkDuplicateAttribute(attributeName, "trusted", tokens, hasTrusted); - checkDuplicateAttribute(attributeName, "system", tokens, hasSystem); - checkDuplicateAttribute(attributeName, "pure", tokens, hasPure); - checkDuplicateAttribute(attributeName, "nothrow", tokens, hasNoThrow); - } - } - - void checkDuplicateAttribute(const string attributeName, - const string attributeDesired, const(Token)[] tokens, ref bool hasAttribute) - { - // Just return if not an attribute - if (attributeName != attributeDesired) - return; - - // Already has that attribute - if (hasAttribute) - { - string message = "Attribute '%s' is duplicated.".format(attributeName); - addErrorMessage(tokens, KEY, message, - [AutoFix.replacement(tokens, "", "Remove second attribute " ~ attributeName)]); - } - - // Mark it as having that attribute - hasAttribute = true; - } - - string getAttributeName(const Attribute attribute, ref const(Token)[] outTokens) - { - // Get the name from the attribute identifier - if (attribute && attribute.atAttribute && attribute.atAttribute.identifier !is Token.init - && attribute.atAttribute.identifier.text - && attribute.atAttribute.identifier.text.length) - { - auto token = attribute.atAttribute.identifier; - outTokens = attribute.atAttribute.tokens; - return token.text; - } - - // Get the attribute from the storage class token - if (attribute && attribute.attribute.type != tok!"") - { - outTokens = attribute.tokens; - return attribute.attribute.type.str; - } - - return null; - } - - string getAttributeName(const MemberFunctionAttribute memberFunctionAttribute, - ref const(Token)[] outTokens) - { - // Get the name from the tokenType - if (memberFunctionAttribute && memberFunctionAttribute.tokenType !is IdType.init - && memberFunctionAttribute.tokenType.str - && memberFunctionAttribute.tokenType.str.length) - { - outTokens = memberFunctionAttribute.tokens; - return memberFunctionAttribute.tokenType.str; - } - - // Get the name from the attribute identifier - if (memberFunctionAttribute && memberFunctionAttribute.atAttribute - && memberFunctionAttribute.atAttribute.identifier !is Token.init - && memberFunctionAttribute.atAttribute.identifier.type == tok!"identifier" - && memberFunctionAttribute.atAttribute.identifier.text - && memberFunctionAttribute.atAttribute.identifier.text.length) - { - auto iden = memberFunctionAttribute.atAttribute.identifier; - outTokens = memberFunctionAttribute.atAttribute.tokens; - return iden.text; - } - - return null; - } - - private enum string KEY = "dscanner.unnecessary.duplicate_attribute"; -} - -unittest -{ - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - - StaticAnalysisConfig sac = disabledConfig(); - sac.duplicate_attribute = Check.enabled; - assertAnalyzerWarnings(q{ - class ExampleAttributes - { - @property @safe bool xxx() // ok - { - return false; - } - - // Duplicate before - @property @property bool aaa() /+ - ^^^^^^^^^ [warn]: Attribute 'property' is duplicated. +/ - { - return false; - } - - // Duplicate after - bool bbb() @safe @safe /+ - ^^^^^ [warn]: Attribute 'safe' is duplicated. +/ - { - return false; - } - - // Duplicate before and after - @system bool ccc() @system /+ - ^^^^^^^ [warn]: Attribute 'system' is duplicated. +/ - { - return false; - } - - // Duplicate before and after - @trusted bool ddd() @trusted /+ - ^^^^^^^^ [warn]: Attribute 'trusted' is duplicated. +/ - { - return false; - } - } - - class ExamplePureNoThrow - { - pure nothrow bool aaa() // ok - { - return false; - } - - pure pure bool bbb() /+ - ^^^^ [warn]: Attribute 'pure' is duplicated. +/ - { - return false; - } - - bool ccc() pure pure /+ - ^^^^ [warn]: Attribute 'pure' is duplicated. +/ - { - return false; - } - - nothrow nothrow bool ddd() /+ - ^^^^^^^ [warn]: Attribute 'nothrow' is duplicated. +/ - { - return false; - } - - bool eee() nothrow nothrow /+ - ^^^^^^^ [warn]: Attribute 'nothrow' is duplicated. +/ - { - return false; - } - } - }c, sac); - - - assertAutoFix(q{ - class ExampleAttributes - { - @property @property bool aaa() {} // fix - bool bbb() @safe @safe {} // fix - @system bool ccc() @system {} // fix - @trusted bool ddd() @trusted {} // fix - } - - class ExamplePureNoThrow - { - pure pure bool bbb() {} // fix - bool ccc() pure pure {} // fix - nothrow nothrow bool ddd() {} // fix - bool eee() nothrow nothrow {} // fix - } - }c, q{ - class ExampleAttributes - { - @property bool aaa() {} // fix - bool bbb() @safe {} // fix - @system bool ccc() {} // fix - @trusted bool ddd() {} // fix - } - - class ExamplePureNoThrow - { - pure bool bbb() {} // fix - bool ccc() pure {} // fix - nothrow bool ddd() {} // fix - bool eee() nothrow {} // fix - } - }c, sac); - - stderr.writeln("Unittest for DuplicateAttributeCheck passed."); -} diff --git a/src/dscanner/analysis/enumarrayliteral.d b/src/dscanner/analysis/enumarrayliteral.d index 96fcc0ca..cad15be9 100644 --- a/src/dscanner/analysis/enumarrayliteral.d +++ b/src/dscanner/analysis/enumarrayliteral.d @@ -5,74 +5,53 @@ module dscanner.analysis.enumarrayliteral; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import std.algorithm : find, map; -import dsymbol.scope_ : Scope; -void doNothing(string, size_t, size_t, string, bool) +extern (C++) class EnumArrayVisitor(AST) : BaseAnalyzerDmd { -} - -final class EnumArrayLiteralCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"enum_array_literal_check"; - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.performance.enum_array_literal"; + + extern (D) this(string fileName) { - super(args); + super(fileName); } - bool looking; + override void visit(AST.VarDeclaration vd) + { + import dmd.astenums : STC, InitKind; + import std.string : toStringz; - mixin visitTemplate!ClassDeclaration; - mixin visitTemplate!InterfaceDeclaration; - mixin visitTemplate!UnionDeclaration; - mixin visitTemplate!StructDeclaration; + string message = "This enum may lead to unnecessary allocation at run-time. Use 'static immutable " + ~ vd.ident.toString().idup() ~ " = [ ...' instead."; - override void visit(const AutoDeclaration autoDec) - { - auto enumToken = autoDec.storageClasses.find!(a => a.token == tok!"enum"); - if (enumToken.length) + if (!vd.type && vd._init.kind == InitKind.array && vd.storage_class & STC.manifest) { - foreach (part; autoDec.parts) - { - if (part.initializer is null) - continue; - if (part.initializer.nonVoidInitializer is null) - continue; - if (part.initializer.nonVoidInitializer.arrayInitializer is null) - continue; - addErrorMessage(part.initializer.nonVoidInitializer, - KEY, - "This enum may lead to unnecessary allocation at run-time." - ~ " Use 'static immutable " - ~ part.identifier.text ~ " = [ ...' instead.", - [ - AutoFix.replacement(enumToken[0].token, "static immutable") - ]); - } + auto fileOffset = vd.loc.fileOffset - 5; + + addErrorMessage( + cast(ulong) vd.loc.linnum, cast(ulong) vd.loc.charnum, KEY, message, + [AutoFix.replacement(fileOffset, fileOffset + 4, "static immutable", "Replace enum with static immutable")] + ); } - autoDec.accept(this); - } - private enum string KEY = "dscanner.performance.enum_array_literal"; + super.visit(vd); + } } unittest { import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.enum_array_literal_check = Check.enabled; - assertAnalyzerWarnings(q{ - enum x = [1, 2, 3]; /+ - ^^^^^^^^^ [warn]: This enum may lead to unnecessary allocation at run-time. Use 'static immutable x = [ ...' instead. +/ + + assertAnalyzerWarningsDMD(q{ + enum x = [1, 2, 3]; // [warn]: This enum may lead to unnecessary allocation at run-time. Use 'static immutable x = [ ...' instead. }c, sac); assertAutoFix(q{ diff --git a/src/dscanner/analysis/explicitly_annotated_unittests.d b/src/dscanner/analysis/explicitly_annotated_unittests.d index 7ff1f156..b80b1265 100644 --- a/src/dscanner/analysis/explicitly_annotated_unittests.d +++ b/src/dscanner/analysis/explicitly_annotated_unittests.d @@ -4,103 +4,78 @@ module dscanner.analysis.explicitly_annotated_unittests; -import dparse.lexer; -import dparse.ast; import dscanner.analysis.base; -import std.stdio; - /** * Requires unittests to be explicitly annotated with either @safe or @system */ -final class ExplicitlyAnnotatedUnittestCheck : BaseAnalyzer +extern (C++) class ExplicitlyAnnotatedUnittestCheck(AST) : BaseAnalyzerDmd { - enum string KEY = "dscanner.style.explicitly_annotated_unittest"; - enum string MESSAGE = "A unittest should be annotated with at least @safe or @system"; + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"explicitly_annotated_unittests"; - /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); } - override void visit(const Declaration decl) + override void visit(AST.UnitTestDeclaration d) { - if (decl.unittest_ !is null) - { - bool isSafeOrSystem; - if (decl.attributes !is null) - foreach (attribute; decl.attributes) - { - if (attribute.atAttribute !is null) - { - const token = attribute.atAttribute.identifier.text; - if (token == "safe" || token == "system") - { - isSafeOrSystem = true; - break; - } - } - } - if (!isSafeOrSystem) - { - auto token = decl.unittest_.findTokenForDisplay(tok!"unittest"); - addErrorMessage(token, KEY, MESSAGE, - [ - AutoFix.insertionBefore(token[0], "@safe ", "Mark unittest @safe"), - AutoFix.insertionBefore(token[0], "@system ", "Mark unittest @system") - ]); - } - } - decl.accept(this); - } + import dmd.astenums : STC; + + if (skipTests) + return; - alias visit = BaseAnalyzer.visit; + if (!(d.storage_class & STC.safe || d.storage_class & STC.system)) + addErrorMessage( + cast(ulong) d.loc.linnum, cast(ulong) d.loc.charnum, KEY, MESSAGE, + [ + AutoFix.insertionAt(d.loc.fileOffset, "@safe "), + AutoFix.insertionAt(d.loc.fileOffset, "@system ") + ] + ); + + super.visit(d); + } +private: + enum string KEY = "dscanner.style.explicitly_annotated_unittest"; + enum string MESSAGE = "A unittest should be annotated with at least @safe or @system"; } unittest { - import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; - import std.format : format; + import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; import std.stdio : stderr; + import std.format : format; StaticAnalysisConfig sac = disabledConfig(); sac.explicitly_annotated_unittests = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ + + @disable foo() {} + @safe unittest {} @system unittest {} pure nothrow @system @nogc unittest {} - unittest {} /+ - ^^^^^^^^ [warn]: %s +/ - pure nothrow @nogc unittest {} /+ - ^^^^^^^^ [warn]: %s +/ - }c.format( - ExplicitlyAnnotatedUnittestCheck.MESSAGE, - ExplicitlyAnnotatedUnittestCheck.MESSAGE, - ), sac); + unittest {} // [warn]: A unittest should be annotated with at least @safe or @system + pure nothrow @nogc unittest {} // [warn]: A unittest should be annotated with at least @safe or @system + }c, sac); // nested - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct Foo { @safe unittest {} @system unittest {} - unittest {} /+ - ^^^^^^^^ [warn]: %s +/ - pure nothrow @nogc unittest {} /+ - ^^^^^^^^ [warn]: %s +/ + unittest {} // [warn]: A unittest should be annotated with at least @safe or @system + pure nothrow @nogc unittest {} // [warn]: A unittest should be annotated with at least @safe or @system } - }c.format( - ExplicitlyAnnotatedUnittestCheck.MESSAGE, - ExplicitlyAnnotatedUnittestCheck.MESSAGE, - ), sac); - + }c, sac); // nested assertAutoFix(q{ diff --git a/src/dscanner/analysis/final_attribute.d b/src/dscanner/analysis/final_attribute.d index 0548f8a5..01893144 100644 --- a/src/dscanner/analysis/final_attribute.d +++ b/src/dscanner/analysis/final_attribute.d @@ -6,37 +6,23 @@ module dscanner.analysis.final_attribute; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; +import dmd.astcodegen; +import dmd.dsymbol; +import dmd.tokens : Token, TOK; +import std.algorithm; +import std.array; +import std.range; +import std.string : format; /** * Checks for useless usage of the final attribute. * * There are several cases where the compiler allows them even if it's a noop. */ -final class FinalAttributeChecker : BaseAnalyzer +extern (C++) class FinalAttributeChecker(AST) : BaseAnalyzerDmd { - -private: - - enum string KEY = "dscanner.useless.final"; - enum string MSGB = "Useless final attribute, %s"; - - static struct MESSAGE - { - static immutable struct_i = "structs cannot be subclassed"; - static immutable union_i = "unions cannot be subclassed"; - static immutable class_t = "templated functions declared within a class are never virtual"; - static immutable class_p = "private functions declared within a class are never virtual"; - static immutable class_f = "functions declared within a final class are never virtual"; - static immutable class_s = "static functions are never virtual"; - static immutable interface_t = "templated functions declared within an interface are never virtual"; - static immutable struct_f = "functions declared within a struct are never virtual"; - static immutable union_f = "functions declared within an union are never virtual"; - static immutable func_n = "nested functions are never virtual"; - static immutable func_g = "global functions are never virtual"; - } + alias visit = BaseAnalyzerDmd.visit; + mixin AnalyzerInfo!"final_attribute_check"; enum Parent { @@ -49,23 +35,13 @@ private: } bool _private; - bool _finalAggregate; + bool _inFinalClass; bool _alwaysStatic; bool _blockStatic; + bool _blockFinal; Parent _parent = Parent.module_; - void addError(T)(const Token finalToken, T t, string msg) - { - import std.format : format; - addErrorMessage(finalToken.type ? finalToken : t.name, KEY, MSGB.format(msg), - [AutoFix.replacement(finalToken, "")]); - } - -public: - - alias visit = BaseAnalyzer.visit; - - mixin AnalyzerInfo!"final_attribute_check"; + Token[] tokens; enum pushPopPrivate = q{ const bool wasPrivate = _private; @@ -73,234 +49,271 @@ public: scope (exit) _private = wasPrivate; }; - /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); + lexFile(); } - override void visit(const(StructDeclaration) sd) + private void lexFile() { - mixin (pushPopPrivate); - const Parent saved = _parent; - _parent = Parent.struct_; - _alwaysStatic = false; - sd.accept(this); - _parent = saved; + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + while (lexer.nextToken() != TOK.endOfFile) + tokens ~= lexer.token; } - override void visit(const(InterfaceDeclaration) id) + override void visit(AST.StorageClassDeclaration scd) { - mixin (pushPopPrivate); - const Parent saved = _parent; - _parent = Parent.interface_; - _alwaysStatic = false; - id.accept(this); - _parent = saved; + import dmd.astenums : STC; + + if (scd.stc & STC.static_) + _blockStatic = true; + + scope (exit) _blockStatic = false; + + if (scd.stc & STC.final_) + _blockFinal = true; + + scope (exit) _blockFinal = false; + + if (!scd.decl) + return; + + foreach (member; *scd.decl) + { + auto sd = member.isStructDeclaration(); + auto ud = member.isUnionDeclaration(); + + if ((scd.stc & STC.final_) != 0) + { + auto finalTokenOffset = tokens.filter!(t => t.loc.linnum == member.loc.linnum) + .find!(t => t.value == TOK.final_) + .front.loc.fileOffset; + + ulong lineNum = cast(ulong) member.loc.linnum; + ulong charNum = cast(ulong) member.loc.charnum; + + AutoFix fix = AutoFix.replacement(finalTokenOffset, finalTokenOffset + 6, "", "Remove final attribute"); + + if (!ud && sd) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.struct_i), [fix]); + + if (ud) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.union_i), [fix]); + } + + member.accept(this); + } } - override void visit(const(UnionDeclaration) ud) + override void visit(AST.TemplateDeclaration td) { - mixin (pushPopPrivate); - const Parent saved = _parent; - _parent = Parent.union_; - _alwaysStatic = false; - ud.accept(this); - _parent = saved; + import dmd.astenums : STC; + + if (!td.members) + return; + + foreach (member; *td.members) + { + auto fd = member.isFuncDeclaration(); + + if (fd && (fd.storage_class & STC.final_)) + { + auto finalTokenOffset = tokens.filter!(t => t.loc.linnum == fd.loc.linnum) + .find!(t => t.value == TOK.final_) + .front.loc.fileOffset; + + ulong lineNum = cast(ulong) fd.loc.linnum; + ulong charNum = cast(ulong) fd.loc.charnum; + + AutoFix fix = AutoFix.replacement(finalTokenOffset, finalTokenOffset + 6, "", "Remove final attribute"); + + if (_parent == Parent.class_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.class_t), [fix]); + + if (_parent == Parent.interface_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.interface_t), [fix]); + } + } + } - override void visit(const(ClassDeclaration) cd) + override void visit(AST.ClassDeclaration cd) { + if (_blockFinal && !_inFinalClass) + _inFinalClass = true; + else if (_inFinalClass) + _inFinalClass = false; + _blockStatic = false; + mixin (pushPopPrivate); const Parent saved = _parent; _parent = Parent.class_; - _alwaysStatic = false; - cd.accept(this); + super.visit(cd); _parent = saved; + _inFinalClass = false; } - override void visit(const(MixinTemplateDeclaration) mtd) + override void visit(AST.FuncDeclaration fd) { - // can't really know where it'll be mixed (class |final class | struct ?) - } - - override void visit(const(TemplateDeclaration) mtd) - { - // regular template are also mixable - } - - override void visit(const(AttributeDeclaration) decl) - { - if (_parent == Parent.class_ && decl.attribute && - decl.attribute.attribute == tok!"static") - _alwaysStatic = true; - } - - override void visit(const(Declaration) d) - { - import std.algorithm.iteration : filter; - import std.algorithm.searching : canFind; + import dmd.astenums : STC; - const Parent savedParent = _parent; - - bool undoBlockStatic; - if (_parent == Parent.class_ && d.attributes && - d.attributes.canFind!(a => a.attribute == tok!"static")) + if ((fd.storage_class & STC.final_) != 0) { - _blockStatic = true; - undoBlockStatic = true; - } + auto finalTokenOffset = tokens.filter!(t => t.loc.linnum == fd.loc.linnum) + .array() + .retro() + .find!(t => t.value == TOK.final_) + .front.loc.fileOffset; - const bool wasFinalAggr = _finalAggregate; - scope(exit) - { - d.accept(this); - _parent = savedParent; - if (undoBlockStatic) - _blockStatic = false; - _finalAggregate = wasFinalAggr; - } + ulong lineNum = cast(ulong) fd.loc.linnum; + ulong charNum = cast(ulong) fd.loc.charnum; - if (!d.attributeDeclaration && - !d.classDeclaration && - !d.structDeclaration && - !d.unionDeclaration && - !d.interfaceDeclaration && - !d.functionDeclaration) - return; + AutoFix fix = AutoFix.replacement(finalTokenOffset, finalTokenOffset + 6, "", "Remove final attribute"); - if (d.attributeDeclaration && d.attributeDeclaration.attribute) - { - const tp = d.attributeDeclaration.attribute.attribute.type; - _private = isProtection(tp) & (tp == tok!"private"); - } + if (_parent == Parent.class_ && _private) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.class_p), [fix]); + else if (fd.storage_class & STC.static_ || _blockStatic) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.class_s), [fix]); + else if (_parent == Parent.class_ && _inFinalClass) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.class_f), [fix]); - const bool isFinal = d.attributes - .canFind!(a => a.attribute.type == tok!"final"); - const Token finalToken = isFinal - ? d.attributes - .filter!(a => a.attribute.type == tok!"final") - .front.attribute - : Token.init; + if (_parent == Parent.struct_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.struct_f), [fix]); - const bool isStaticOnce = d.attributes - .canFind!(a => a.attribute.type == tok!"static"); + if (_parent == Parent.union_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.union_f), [fix]); - // determine if private - const bool changeProtectionOnce = d.attributes - .canFind!(a => a.attribute.type.isProtection); + if (_parent == Parent.module_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.func_g), [fix]); - const bool isPrivateOnce = d.attributes - .canFind!(a => a.attribute.type == tok!"private"); + if (_parent == Parent.function_) + addErrorMessage(lineNum, charNum, KEY, MSGB.format(FinalAttributeChecker.MESSAGE.func_n), [fix]); + } - bool isPrivate; + _blockStatic = false; + mixin (pushPopPrivate); + const Parent saved = _parent; + _parent = Parent.function_; + super.visit(fd); + _parent = saved; + } - if (isPrivateOnce) - isPrivate = true; - else if (_private && !changeProtectionOnce) - isPrivate = true; + override void visit(AST.InterfaceDeclaration id) + { + _blockStatic = false; + mixin (pushPopPrivate); + const Parent saved = _parent; + _parent = Parent.interface_; + super.visit(id); + _parent = saved; + } - // check final aggregate type - if (d.classDeclaration || d.structDeclaration || d.unionDeclaration) - { - _finalAggregate = isFinal; - if (_finalAggregate && savedParent == Parent.module_) - { - if (d.structDeclaration) - addError(finalToken, d.structDeclaration, MESSAGE.struct_i); - else if (d.unionDeclaration) - addError(finalToken, d.unionDeclaration, MESSAGE.union_i); - } - } + override void visit(AST.UnionDeclaration ud) + { + _blockStatic = false; + mixin (pushPopPrivate); + const Parent saved = _parent; + _parent = Parent.union_; + super.visit(ud); + _parent = saved; + } - if (!d.functionDeclaration) - return; + override void visit(AST.StructDeclaration sd) + { + _blockStatic = false; + mixin (pushPopPrivate); + const Parent saved = _parent; + _parent = Parent.struct_; + super.visit(sd); + _parent = saved; + } - // check final functions - _parent = Parent.function_; - const(FunctionDeclaration) fd = d.functionDeclaration; + override void visit(AST.VisibilityDeclaration vd) + { + if (vd.visibility.kind == Visibility.Kind.private_) + _private = true; + else + _private = false; + + super.visit(vd); + _private = false; + } - if (isFinal) final switch(savedParent) - { - case Parent.class_: - if (fd.templateParameters) - addError(finalToken, fd, MESSAGE.class_t); - if (isPrivate) - addError(finalToken, fd, MESSAGE.class_p); - else if (isStaticOnce || _alwaysStatic || _blockStatic) - addError(finalToken, fd, MESSAGE.class_s); - else if (_finalAggregate) - addError(finalToken, fd, MESSAGE.class_f); - break; - case Parent.interface_: - if (fd.templateParameters) - addError(finalToken, fd, MESSAGE.interface_t); - break; - case Parent.struct_: - addError(finalToken, fd, MESSAGE.struct_f); - break; - case Parent.union_: - addError(finalToken, fd, MESSAGE.union_f); - break; - case Parent.function_: - addError(finalToken, fd, MESSAGE.func_n); - break; - case Parent.module_: - addError(finalToken, fd, MESSAGE.func_g); - break; - } + enum KEY = "dscanner.useless.final"; + enum string MSGB = "Useless final attribute, %s"; + extern(D) static struct MESSAGE + { + static immutable struct_i = "structs cannot be subclassed"; + static immutable union_i = "unions cannot be subclassed"; + static immutable class_t = "templated functions declared within a class are never virtual"; + static immutable class_p = "private functions declared within a class are never virtual"; + static immutable class_f = "functions declared within a final class are never virtual"; + static immutable class_s = "static functions are never virtual"; + static immutable interface_t = "templated functions declared within an interface are never virtual"; + static immutable struct_f = "functions declared within a struct are never virtual"; + static immutable union_f = "functions declared within an union are never virtual"; + static immutable func_n = "nested functions are never virtual"; + static immutable func_g = "global functions are never virtual"; } } @system unittest { import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; - import std.format : format; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.final_attribute_check = Check.enabled; - - // pass - - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ void foo(){} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo(){void foo(){}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct S{} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ union U{} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo{public final void foo(){}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ final class Foo{static struct Bar{}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo{private: public final void foo(){}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo{private: public: final void foo(){}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo{private: public: final void foo(){}} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Impl { private: @@ -311,7 +324,7 @@ public: } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ mixin template Impl() { protected final void mixin_template_can() {} @@ -320,112 +333,99 @@ public: // fail - assertAnalyzerWarnings(q{ - final void foo(){} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + final void foo(){} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.func_g) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.func_g) ), sac); - assertAnalyzerWarnings(q{ - void foo(){final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + void foo(){final void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.func_n) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.func_n) ), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo() { static if (true) - final class A{ private: final protected void foo(){}} /+ - ^^^^^ [warn]: %s +/ + final class A{ private: final protected void foo(){}} // [warn]: %s } }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_f) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_f) ), sac); - assertAnalyzerWarnings(q{ - final struct Foo{} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + final struct Foo{} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.struct_i) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.struct_i) ), sac); - assertAnalyzerWarnings(q{ - final union Foo{} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + final union Foo{} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.union_i) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.union_i) ), sac); - assertAnalyzerWarnings(q{ - class Foo{private final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + class Foo{private final void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_p) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_p) ), sac); - assertAnalyzerWarnings(q{ - class Foo{private: final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + class Foo{private: final void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_p) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_p) ), sac); - assertAnalyzerWarnings(q{ - interface Foo{final void foo(T)(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + interface Foo{final void foo(T)(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.interface_t) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.interface_t) ), sac); - assertAnalyzerWarnings(q{ - final class Foo{final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + final class Foo{final void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_f) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_f) ), sac); - assertAnalyzerWarnings(q{ - private: final class Foo {public: private final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + private: final class Foo {public: private final void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_p) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_p) ), sac); - assertAnalyzerWarnings(q{ - class Foo {final static void foo(){}} /+ - ^^^^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + class Foo {final static void foo(){}} // [warn]: %s }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_s) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_s) ), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo { void foo(){} - static: final void foo(){} /+ - ^^^^^ [warn]: %s +/ + static: final void foo(){} // [warn]: %s } }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_s) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_s) ), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo { void foo(){} - static{ final void foo(){}} /+ - ^^^^^ [warn]: %s +/ + static{ final void foo(){}} // [warn]: %s void foo(){} } }c.format( - FinalAttributeChecker.MSGB.format(FinalAttributeChecker.MESSAGE.class_s) + (FinalAttributeChecker!ASTCodegen).MSGB.format((FinalAttributeChecker!ASTCodegen).MESSAGE.class_s) ), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Statement { final class UsesEH{} @@ -433,7 +433,6 @@ public: } }, sac); - assertAutoFix(q{ final void foo(){} // fix void foo(){final void foo(){}} // fix @@ -458,12 +457,12 @@ public: class Foo { void foo(){} - static{ final void foo(){}} // fix + static{final void foo(){}} // fix void foo(){} } }, q{ void foo(){} // fix - void foo(){ void foo(){}} // fix + void foo(){void foo(){}} // fix void foo() { static if (true) @@ -473,10 +472,10 @@ public: union Foo{} // fix class Foo{private void foo(){}} // fix class Foo{private: void foo(){}} // fix - interface Foo{ void foo(T)(){}} // fix - final class Foo{ void foo(){}} // fix + interface Foo{void foo(T)(){}} // fix + final class Foo{void foo(){}} // fix private: final class Foo {public: private void foo(){}} // fix - class Foo { static void foo(){}} // fix + class Foo {static void foo(){}} // fix class Foo { void foo(){} @@ -485,7 +484,7 @@ public: class Foo { void foo(){} - static{ void foo(){}} // fix + static{void foo(){}} // fix void foo(){} } }, sac); diff --git a/src/dscanner/analysis/fish.d b/src/dscanner/analysis/fish.d deleted file mode 100644 index c88ff77c..00000000 --- a/src/dscanner/analysis/fish.d +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright Brian Schott (Hackerpilot) 2014. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) - -module dscanner.analysis.fish; - -import std.stdio; -import dparse.ast; -import dparse.lexer; -import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; - -/** - * Checks for use of the deprecated floating point comparison operators. - */ -final class FloatOperatorCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - - enum string KEY = "dscanner.deprecated.floating_point_operators"; - mixin AnalyzerInfo!"float_operator_check"; - - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const RelExpression r) - { - if (r.operator == tok!"<>" || r.operator == tok!"<>=" - || r.operator == tok!"!<>" || r.operator == tok!"!>" - || r.operator == tok!"!<" || r.operator == tok!"!<>=" - || r.operator == tok!"!>=" || r.operator == tok!"!<=") - { - addErrorMessage(r, KEY, - "Avoid using the deprecated floating-point operators."); - } - r.accept(this); - } -} - -unittest -{ - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - - StaticAnalysisConfig sac = disabledConfig(); - sac.float_operator_check = Check.enabled; - assertAnalyzerWarnings(q{ - void testFish() - { - float z = 1.5f; - bool a; - a = z !<>= z; /+ - ^^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z !<> z; /+ - ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z <> z; /+ - ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z <>= z; /+ - ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z !> z; /+ - ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z !>= z; /+ - ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z !< z; /+ - ^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - a = z !<= z; /+ - ^^^^^^^ [warn]: Avoid using the deprecated floating-point operators. +/ - } - }c, sac); - - stderr.writeln("Unittest for FloatOperatorCheck passed."); -} diff --git a/src/dscanner/analysis/function_attributes.d b/src/dscanner/analysis/function_attributes.d index 9f106d8a..f74601dc 100644 --- a/src/dscanner/analysis/function_attributes.d +++ b/src/dscanner/analysis/function_attributes.d @@ -6,11 +6,9 @@ module dscanner.analysis.function_attributes; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; -import std.stdio; -import dsymbol.scope_; +import dmd.astenums : STC, MOD, MODFlags; +import dmd.tokens : Token, TOK; +import std.string : format; /** * Prefer @@ -22,207 +20,256 @@ import dsymbol.scope_; * const int getStuff() {} * --- */ -final class FunctionAttributeCheck : BaseAnalyzer +extern (C++) class FunctionAttributeCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"function_attribute_check"; - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum KEY = "dscanner.confusing.function_attributes"; + private enum CONST_MSG = "Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'."; + private enum ABSTRACT_MSG = "'abstract' attribute is redundant in interface declarations"; + private enum RETURN_MSG = "'%s' is not an attribute of the return type. Place it after the parameter list to clarify."; - override void visit(const InterfaceDeclaration dec) - { - const t = inInterface; - const t2 = inAggregate; - inInterface = true; - inAggregate = true; - dec.accept(this); - inInterface = t; - inAggregate = t2; - } + private bool inInterface = false; + private bool inAggregate = false; + private Token[] tokens; - override void visit(const ClassDeclaration dec) + extern (D) this(string fileName, bool skipTests = false) { - const t = inInterface; - const t2 = inAggregate; - inInterface = false; - inAggregate = true; - dec.accept(this); - inInterface = t; - inAggregate = t2; + super(fileName, skipTests); + getTokens(); } - override void visit(const StructDeclaration dec) + private void getTokens() { - const t = inInterface; - const t2 = inAggregate; - inInterface = false; - inAggregate = true; - dec.accept(this); - inInterface = t; - inAggregate = t2; + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + while (lexer.nextToken() != TOK.endOfFile) + tokens ~= lexer.token; } - override void visit(const UnionDeclaration dec) - { - const t = inInterface; - const t2 = inAggregate; - inInterface = false; - inAggregate = true; - dec.accept(this); - inInterface = t; - inAggregate = t2; - } + mixin visitAggregate!(AST.InterfaceDeclaration, true); + mixin visitAggregate!(AST.ClassDeclaration); + mixin visitAggregate!(AST.StructDeclaration); + mixin visitAggregate!(AST.UnionDeclaration); - override void visit(const AttributeDeclaration dec) + private template visitAggregate(NodeType, bool isInterface = false) { - if (inInterface && dec.attribute.attribute == tok!"abstract") + override void visit(NodeType node) { - addErrorMessage(dec.attribute, KEY, ABSTRACT_MESSAGE); + immutable bool oldInAggregate = inAggregate; + immutable bool oldInInterface = inInterface; + + inAggregate = !isStaticAggregate(node.loc.linnum, node.loc.charnum); + inInterface = isInterface; + super.visit(node); + + inAggregate = oldInAggregate; + inInterface = oldInInterface; } } - override void visit(const FunctionDeclaration dec) + private bool isStaticAggregate(uint lineNum, uint charNum) { - if (dec.parameters.parameters.length == 0 && inAggregate) - { - bool foundConst; - bool foundProperty; - foreach (attribute; dec.attributes) - foundConst = foundConst || attribute.attribute.type == tok!"const" - || attribute.attribute.type == tok!"immutable" - || attribute.attribute.type == tok!"inout"; - foreach (attribute; dec.memberFunctionAttributes) - { - foundProperty = foundProperty || (attribute.atAttribute !is null - && attribute.atAttribute.identifier.text == "property"); - foundConst = foundConst || attribute.tokenType == tok!"const" - || attribute.tokenType == tok!"immutable" || attribute.tokenType == tok!"inout"; - } - if (foundProperty && !foundConst) - { - auto paren = dec.parameters.tokens.length ? dec.parameters.tokens[$ - 1] : Token.init; - auto autofixes = paren is Token.init ? null : [ - AutoFix.insertionAfter(paren, " const", "Mark function `const`"), - AutoFix.insertionAfter(paren, " inout", "Mark function `inout`"), - AutoFix.insertionAfter(paren, " immutable", "Mark function `immutable`"), - ]; - addErrorMessage(dec.name, KEY, - "Zero-parameter '@property' function should be" - ~ " marked 'const', 'inout', or 'immutable'.", autofixes); - } - } - dec.accept(this); + import std.algorithm : any, filter; + + return tokens.filter!(token => token.loc.linnum == lineNum && token.loc.charnum <= charNum) + .filter!(token => token.value >= TOK.struct_ && token.value <= TOK.immutable_) + .any!(token => token.value == TOK.static_); } - override void visit(const Declaration dec) + override void visit(AST.FuncDeclaration fd) { - bool isStatic = false; - if (dec.attributes.length == 0) - goto end; - foreach (attr; dec.attributes) + import std.algorithm : canFind, find, filter, until; + import std.array : array; + import std.range : retro; + + super.visit(fd); + + if (fd.type is null) + return; + + string funcName = fd.ident is null ? "" : cast(string) fd.ident.toString(); + ulong fileOffset = cast(ulong) fd.loc.fileOffset; + ulong lineNum = cast(ulong) fd.loc.linnum; + ulong charNum = cast(ulong) fd.loc.charnum; + ulong[2] index = [fileOffset, fileOffset + funcName.length]; + ulong[2] lines = [lineNum, lineNum]; + ulong[2] columns = [charNum, charNum + funcName.length]; + + if (inInterface) { - if (attr.attribute.type == tok!"") - continue; - if (attr.attribute == tok!"abstract" && inInterface) + immutable bool isAbstract = (fd.storage_class & STC.abstract_) > 0; + if (isAbstract) { - addErrorMessage(attr.attribute, KEY, ABSTRACT_MESSAGE, - [AutoFix.replacement(attr.attribute, "")]); - continue; + auto offset = tokens.filter!(t => t.loc.linnum >= fd.loc.linnum) + .until!(t => t.value == TOK.leftCurly) + .array + .retro() + .find!(t => t.value == TOK.abstract_) + .front.loc.fileOffset; + + addErrorMessage( + index, lines, columns, KEY, ABSTRACT_MSG, + [AutoFix.replacement(offset, offset + 8, "", "Remove `abstract` attribute")] + ); + + return; } - if (attr.attribute == tok!"static") + } + + auto tf = fd.type.isTypeFunction(); + + if (inAggregate && tf) + { + string storageTok = getConstLikeStorage(tf.mod); + auto bodyStartToken = TOK.leftCurly; + if (fd.fbody is null) + bodyStartToken = TOK.semicolon; + + Token[] funcTokens = tokens.filter!(t => t.loc.fileOffset > fd.loc.fileOffset) + .until!(t => t.value == TOK.leftCurly || t.value == bodyStartToken) + .array; + + if (storageTok is null) { - isStatic = true; + bool isStatic = (fd.storage_class & STC.static_) > 0; + bool isZeroParamProperty = tf.isProperty() && tf.parameterList.parameters.length == 0; + auto propertyRange = funcTokens.retro() + .until!(t => t.value == TOK.rightParenthesis) + .find!(t => t.ident.toString() == "property") + .find!(t => t.value == TOK.at); + + if (!isStatic && isZeroParamProperty && !propertyRange.empty) + addErrorMessage( + index, lines, columns, KEY, CONST_MSG, + [ + AutoFix.insertionAt(propertyRange.front.loc.fileOffset, "const "), + AutoFix.insertionAt(propertyRange.front.loc.fileOffset, "inout "), + AutoFix.insertionAt(propertyRange.front.loc.fileOffset, "immutable "), + ] + ); } - if (dec.functionDeclaration !is null && (attr.attribute == tok!"const" - || attr.attribute == tok!"inout" || attr.attribute == tok!"immutable")) + else { - import std.string : format; - - immutable string attrString = str(attr.attribute.type); - AutoFix[] autofixes; - if (dec.functionDeclaration.parameters) - autofixes ~= AutoFix.replacement( - attr.attribute, "", - "Move " ~ str(attr.attribute.type) ~ " after parameter list") - .concat(AutoFix.insertionAfter( - dec.functionDeclaration.parameters.tokens[$ - 1], - " " ~ str(attr.attribute.type))); - if (dec.functionDeclaration.returnType) - autofixes ~= AutoFix.insertionAfter(attr.attribute, "(", "Make return type const") - .concat(AutoFix.insertionAfter(dec.functionDeclaration.returnType.tokens[$ - 1], ")")); - addErrorMessage(attr.attribute, KEY, format( - "'%s' is not an attribute of the return type." - ~ " Place it after the parameter list to clarify.", - attrString), autofixes); + bool hasConstLikeAttribute = funcTokens.retro() + .canFind!(t => t.value == TOK.const_ || t.value == TOK.immutable_ || t.value == TOK.inout_); + + if (!hasConstLikeAttribute) + { + auto funcRange = tokens.filter!(t => t.loc.linnum >= fd.loc.linnum) + .until!(t => t.value == TOK.leftCurly || t.value == TOK.semicolon); + auto parensToken = funcRange.until!(t => t.value == TOK.leftParenthesis) + .array + .retro() + .front; + auto funcEndToken = funcRange.array + .retro() + .find!(t => t.value == TOK.rightParenthesis) + .front; + auto constLikeToken = funcRange + .find!(t => t.value == TOK.const_ || t.value == TOK.inout_ || t.value == TOK.immutable_) + .front; + + string modifier; + if (constLikeToken.value == TOK.const_) + modifier = " const"; + else if (constLikeToken.value == TOK.inout_) + modifier = " inout"; + else + modifier = " immutable"; + + AutoFix fix1 = AutoFix + .replacement(constLikeToken.loc.fileOffset, constLikeToken.loc.fileOffset + modifier.length, + "", "Move" ~ modifier ~ " after parameter list") + .concat(AutoFix.insertionAt(funcEndToken.loc.fileOffset + 1, modifier)); + + AutoFix fix2 = AutoFix.replacement(constLikeToken.loc.fileOffset + modifier.length - 1, + constLikeToken.loc.fileOffset + modifier.length, "(", "Make return type" ~ modifier) + .concat(AutoFix.insertionAt(parensToken.loc.fileOffset - 1, ")")); + + addErrorMessage(index, lines, columns, KEY, RETURN_MSG.format(storageTok), [fix1, fix2]); + } } } - end: - if (isStatic) { - const t = inAggregate; - inAggregate = false; - dec.accept(this); - inAggregate = t; - } - else { - dec.accept(this); - } } -private: - bool inInterface; - bool inAggregate; - enum string ABSTRACT_MESSAGE = "'abstract' attribute is redundant in interface declarations"; - enum string KEY = "dscanner.confusing.function_attributes"; + private extern (D) string getConstLikeStorage(MOD mod) + { + if (mod & MODFlags.const_) + return "const"; + + if (mod & MODFlags.immutable_) + return "immutable"; + + if (mod & MODFlags.wild) + return "inout"; + + return null; + } } unittest { - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.function_attribute_check = Check.enabled; - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ int foo() @property { return 0; } class ClassName { - const int confusingConst() { return 0; } /+ - ^^^^^ [warn]: 'const' is not an attribute of the return type. Place it after the parameter list to clarify. +/ - - int bar() @property { return 0; } /+ - ^^^ [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. +/ + const int confusingConst() { return 0; } // [warn]: 'const' is not an attribute of the return type. Place it after the parameter list to clarify. + int bar() @property { return 0; } // [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. static int barStatic() @property { return 0; } int barConst() const @property { return 0; } } struct StructName { - int bar() @property { return 0; } /+ - ^^^ [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. +/ + int bar() @property { return 0; } // [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. static int barStatic() @property { return 0; } int barConst() const @property { return 0; } } union UnionName { - int bar() @property { return 0; } /+ - ^^^ [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. +/ + int bar() @property { return 0; } // [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. static int barStatic() @property { return 0; } int barConst() const @property { return 0; } } interface InterfaceName { - int bar() @property; /+ - ^^^ [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. +/ + int bar() @property; // [warn]: Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'. static int barStatic() @property { return 0; } int barConst() const @property; - - abstract int method(); /+ - ^^^^^^^^ [warn]: 'abstract' attribute is redundant in interface declarations +/ + abstract int method(); // [warn]: 'abstract' attribute is redundant in interface declarations } }c, sac); + // Test taken from phobos / utf.d, shouldn't warn + assertAnalyzerWarningsDMD(q{ + static struct R + { + @safe pure @nogc nothrow: + this(string s) { this.s = s; } + @property bool empty() { return idx == s.length; } + @property char front() { return s[idx]; } + void popFront() { ++idx; } + size_t idx; + string s; + } + }c, sac); assertAutoFix(q{ int foo() @property { return 0; } @@ -276,5 +323,5 @@ unittest } }c, sac); - stderr.writeln("Unittest for FunctionAttributeCheck passed."); + stderr.writeln("Unittest for ObjectConstCheck passed."); } diff --git a/src/dscanner/analysis/has_public_example.d b/src/dscanner/analysis/has_public_example.d index d8f21b78..b8b2e34d 100644 --- a/src/dscanner/analysis/has_public_example.d +++ b/src/dscanner/analysis/has_public_example.d @@ -5,184 +5,168 @@ module dscanner.analysis.has_public_example; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; -import dparse.ast; -import dparse.lexer; - -import std.algorithm; -import std.stdio; /** * Checks for public declarations without a documented unittests. * For now, variable and enum declarations aren't checked. */ -final class HasPublicExampleCheck : BaseAnalyzer +extern (C++) class HasPublicExampleCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"has_public_example"; - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum KEY = "dscanner.style.has_public_example"; + private enum DEFAULT_MSG = "Public declaration has no documented example."; + private enum MSG = "Public declaration '%s' has no documented example."; - override void visit(const Module mod) + private struct DeclarationInfo { - // the last seen declaration is memorized - Declaration lastDecl; - - // keep track of ddoced unittests after visiting lastDecl - bool hasNoDdocUnittest; - - // on lastDecl reset we check for seen ddoced unittests since lastDecl was observed - void checkLastDecl() - { - if (lastDecl !is null && hasNoDdocUnittest) - triggerError(lastDecl); - lastDecl = null; - } - - // check all public top-level declarations - foreach (decl; mod.declarations) - { - if (decl.attributes.any!(a => a.deprecated_ !is null)) - { - lastDecl = null; - continue; - } - - if (!isPublic(decl.attributes)) - { - checkLastDecl(); - continue; - } + bool ignore; + string name; + ulong lineNum; + ulong charNum; + } - const bool hasDdocHeader = hasDdocHeader(decl); + private DeclarationInfo lastDecl = DeclarationInfo(true); + private bool isDocumented; - // check the documentation of a unittest declaration - if (decl.unittest_ !is null) - { - if (hasDdocHeader) - hasNoDdocUnittest = false; - } - // add all declarations that could be publicly documented to the lastDecl "stack" - else if (hasDittableDecl(decl)) - { - // ignore dittoed declarations - if (hasDittos(decl)) - continue; - - // new public symbol -> check the previous decl - checkLastDecl; + extern (D) this(string fileName, bool skipTests = false) + { + super(fileName, skipTests); + } - lastDecl = hasDdocHeader ? cast(Declaration) decl : null; - hasNoDdocUnittest = true; - } - else - // ran into variableDeclaration or something else -> reset & validate current lastDecl "stack" - checkLastDecl; - } - checkLastDecl; + override void visit(AST.Module mod) + { + super.visit(mod); + checkLastDecl(); } -private: + override void visit(AST.ConditionalStatement _) {} - enum string KEY = "dscanner.style.has_public_example"; + override void visit(AST.ConditionalDeclaration _) {} - bool hasDitto(Decl)(const Decl decl) + override void visit(AST.UnitTestDeclaration unitTestDecl) { - import ddoc.comments : parseComment; - if (decl is null || decl.comment is null) - return false; + if (skipTests) + return; - return parseComment(decl.comment, null).isDitto; + if (unitTestDecl.comment() !is null) + isDocumented = true; } - bool hasDittos(Decl)(const Decl decl) + override void visit(AST.DeprecatedDeclaration _) { - foreach (property; possibleDeclarations) - if (mixin("hasDitto(decl." ~ property ~ ")")) - return true; - return false; + lastDecl = DeclarationInfo(true); } - bool hasDittableDecl(Decl)(const Decl decl) + override void visit(AST.StorageClassDeclaration storageClassDecl) { - foreach (property; possibleDeclarations) - if (mixin("decl." ~ property ~ " !is null")) - return true; - return false; + if (!hasIgnorableStorageClass(storageClassDecl.stc)) + super.visit(storageClassDecl); + else + lastDecl = DeclarationInfo(true); } - import std.meta : AliasSeq; - alias possibleDeclarations = AliasSeq!( - "classDeclaration", - "enumDeclaration", - "functionDeclaration", - "interfaceDeclaration", - "structDeclaration", - "templateDeclaration", - "unionDeclaration", - //"variableDeclaration", - ); - - bool hasDdocHeader(const Declaration decl) + private bool hasIgnorableStorageClass(ulong storageClass) { - if (decl.declarations !is null) - return false; + import dmd.astenums : STC; - // unittest can have ddoc headers as well, but don't have a name - if (decl.unittest_ !is null && decl.unittest_.comment.ptr !is null) - return true; + return (storageClass & STC.deprecated_) || (storageClass & STC.manifest); + } - foreach (property; possibleDeclarations) - if (mixin("decl." ~ property ~ " !is null && decl." ~ property ~ ".comment.ptr !is null")) - return true; + override void visit(AST.VisibilityDeclaration visibilityDecl) + { + import dmd.dsymbol : Visibility; - return false; + auto visibilityKind = visibilityDecl.visibility.kind; + bool isPrivate = visibilityKind == Visibility.Kind.private_ + || visibilityKind == Visibility.Kind.package_ + || visibilityKind == Visibility.Kind.protected_; + + if (isPrivate) + checkLastDecl(); + else + super.visit(visibilityDecl); } - bool isPublic(const Attribute[] attrs) + mixin VisitDeclaration!(AST.ClassDeclaration); + mixin VisitDeclaration!(AST.InterfaceDeclaration); + mixin VisitDeclaration!(AST.StructDeclaration); + mixin VisitDeclaration!(AST.UnionDeclaration); + mixin VisitDeclaration!(AST.FuncDeclaration); + mixin VisitDeclaration!(AST.TemplateDeclaration); + + private template VisitDeclaration(NodeType) { - import dparse.lexer : tok; + override void visit(NodeType node) + { + import std.conv : to; + import std.string : strip, toLower; - enum tokPrivate = tok!"private", tokProtected = tok!"protected", tokPackage = tok!"package"; + static if (is(NodeType == AST.TemplateDeclaration)) + { + if (shouldTemplateBeSkipped(node)) + return; + } - if (attrs.map!`a.attribute`.any!(x => x == tokPrivate || x == tokProtected || x == tokPackage)) - return false; + bool isCommented = node.comment() !is null; + + if (isCommented) + { + string comment = to!string(node.comment()); + if (comment.strip().toLower() == "ditto") + return; + } - return true; + checkLastDecl(); + + if (isCommented) + { + string name = node.ident ? cast(string) node.ident.toString() : null; + lastDecl = DeclarationInfo(false, name, cast(ulong) node.loc.linnum, cast(ulong) node.loc.charnum); + } + + isDocumented = false; + } } - void triggerError(const Declaration decl) + private bool shouldTemplateBeSkipped(AST.TemplateDeclaration templateDecl) { - foreach (property; possibleDeclarations) - if (auto fn = mixin("decl." ~ property)) - addMessage(fn.name.type ? [fn.name] : fn.tokens, fn.name.text); + if (templateDecl.members is null) + return false; + + foreach (member; *(templateDecl.members)) + if (auto var = member.isVarDeclaration()) + if (hasIgnorableStorageClass(var.storage_class)) + return true; + + return false; } - void addMessage(const Token[] tokens, string name) + private void checkLastDecl() { - import std.string : format; + import std.format : format; + + if (!lastDecl.ignore && !isDocumented) + { + string msg = lastDecl.name ? MSG.format(lastDecl.name) : DEFAULT_MSG; + addErrorMessage(lastDecl.lineNum, lastDecl.charNum, KEY, msg); + } - addErrorMessage(tokens, KEY, name is null - ? "Public declaration has no documented example." - : format("Public declaration '%s' has no documented example.", name)); + lastDecl = DeclarationInfo(true); } } unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.has_public_example = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// C class C{} /// @@ -220,60 +204,51 @@ unittest }, sac); // enums or variables don't need to have public unittest - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// C - class C{} /+ - ^ [warn]: Public declaration 'C' has no documented example. +/ + class C{} // [warn]: Public declaration 'C' has no documented example. unittest {} /// I - interface I{} /+ - ^ [warn]: Public declaration 'I' has no documented example. +/ + interface I{} // [warn]: Public declaration 'I' has no documented example. unittest {} /// f - void f(){} /+ - ^ [warn]: Public declaration 'f' has no documented example. +/ + void f(){} // [warn]: Public declaration 'f' has no documented example. unittest {} /// S - struct S{} /+ - ^ [warn]: Public declaration 'S' has no documented example. +/ + struct S{} // [warn]: Public declaration 'S' has no documented example. unittest {} /// T - template T(){} /+ - ^ [warn]: Public declaration 'T' has no documented example. +/ + template T(){} // [warn]: Public declaration 'T' has no documented example. unittest {} /// U - union U{} /+ - ^ [warn]: Public declaration 'U' has no documented example. +/ + union U{} // [warn]: Public declaration 'U' has no documented example. unittest {} }, sac); // test module header unittest - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ unittest {} /// C - class C{} /+ - ^ [warn]: Public declaration 'C' has no documented example. +/ + class C{} // [warn]: Public declaration 'C' has no documented example. }, sac); // test documented module header unittest - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// unittest {} /// C - class C{} /+ - ^ [warn]: Public declaration 'C' has no documented example. +/ + class C{} // [warn]: Public declaration 'C' has no documented example. }, sac); // test multiple unittest blocks - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// C - class C{} /+ - ^ [warn]: Public declaration 'C' has no documented example. +/ + class C{} // [warn]: Public declaration 'C' has no documented example. unittest {} unittest {} unittest {} @@ -287,7 +262,7 @@ unittest }, sac); /// check private - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// C private class C{} @@ -308,7 +283,7 @@ unittest // check intermediate private declarations // removed for issue #500 - /*assertAnalyzerWarnings(q{ + /*assertAnalyzerWarningsDMD(q{ /// C class C{} private void foo(){} @@ -317,7 +292,7 @@ unittest }, sac);*/ // check intermediate ditto-ed declarations - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// I interface I{} /// ditto @@ -327,21 +302,19 @@ unittest }, sac); // test reset on private symbols (#500) - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// - void dirName(C)(C[] path) {} /+ - ^^^^^^^ [warn]: Public declaration 'dirName' has no documented example. +/ + void dirName(C)(C[] path) {} // [warn]: Public declaration 'dirName' has no documented example. private void _dirName(R)(R path) {} /// unittest {} }, sac); // deprecated symbols shouldn't require a test - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// deprecated void dirName(C)(C[] path) {} }, sac); stderr.writeln("Unittest for HasPublicExampleCheck passed."); } - diff --git a/src/dscanner/analysis/helpers.d b/src/dscanner/analysis/helpers.d index d9ac6581..761efcf5 100644 --- a/src/dscanner/analysis/helpers.d +++ b/src/dscanner/analysis/helpers.d @@ -6,6 +6,8 @@ module dscanner.analysis.helpers; import core.exception : AssertError; +import std.file : exists, remove; +import std.path : dirName; import std.stdio; import std.string; import std.traits; @@ -13,12 +15,17 @@ import std.traits; import dparse.ast; import dparse.lexer : tok, Token; import dparse.rollback_allocator; + import dscanner.analysis.base; import dscanner.analysis.config; import dscanner.analysis.run; -import dsymbol.modulecache : ModuleCache; -import std.experimental.allocator; -import std.experimental.allocator.mallocator; +import dscanner.analysis.rundmd; +import dscanner.utils : getModuleName; + +import dmd.astbase : ASTBase; +import dmd.astcodegen; +import dmd.frontend; +import dmd.parse : Parser; S between(S)(S value, S before, S after) if (isSomeString!S) { @@ -41,179 +48,6 @@ S after(S)(S value, S separator) if (isSomeString!S) return value[i + separator.length .. $]; } -string getLineIndentation(scope const(Token)[] tokens, size_t line, const AutoFixFormatting formatting) -{ - import std.algorithm : countUntil; - import std.array : array; - import std.range : repeat; - import std.string : lastIndexOfAny; - - auto idx = tokens.countUntil!(a => a.line == line); - if (idx == -1 || tokens[idx].column <= 1 || !formatting.indentation.length) - return ""; - - auto indent = tokens[idx].column - 1; - if (formatting.indentation[0] == '\t') - return (cast(immutable)'\t').repeat(indent).array; - else - return (cast(immutable)' ').repeat(indent).array; -} - -/** - * This assert function will analyze the passed in code, get the warnings, - * and make sure they match the warnings in the comments. Warnings are - * marked like so if range doesn't matter: // [warn]: Failed to do somethings. - * - * To test for start and end column, mark warnings as multi-line comments like - * this: /+ - * ^^^^^ [warn]: Failed to do somethings. +/ - */ -void assertAnalyzerWarnings(string code, const StaticAnalysisConfig config, - string file = __FILE__, size_t line = __LINE__) -{ - import dscanner.analysis.run : parseModule; - import dparse.lexer : StringCache, Token; - - StringCache cache = StringCache(StringCache.defaultBucketCount); - RollbackAllocator r; - const(Token)[] tokens; - const(Module) m = parseModule(file, cast(ubyte[]) code, &r, defaultErrorFormat, cache, false, tokens); - - ModuleCache moduleCache; - - // Run the code and get any warnings - MessageSet rawWarnings = analyze("test", m, config, moduleCache, tokens); - string[] codeLines = code.splitLines(); - - struct FoundWarning - { - string msg; - size_t startColumn, endColumn; - } - - // Get the warnings ordered by line - FoundWarning[size_t] warnings; - foreach (rawWarning; rawWarnings[]) - { - // Skip the warning if it is on line zero - immutable size_t rawLine = rawWarning.endLine; - if (rawLine == 0) - { - stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", - rawWarning.message); - continue; - } - - size_t warnLine = line - 1 + rawLine; - warnings[warnLine] = FoundWarning( - format("[warn]: %s", rawWarning.message), - rawWarning.startLine != rawWarning.endLine ? 1 : rawWarning.startColumn, - rawWarning.endColumn, - ); - } - - // Get all the messages from the comments in the code - FoundWarning[size_t] messages; - bool lastLineStartedComment = false; - foreach (i, codeLine; codeLines) - { - scope (exit) - lastLineStartedComment = codeLine.stripRight.endsWith("/+", "/*") > 0; - - // Get the line of this code line - size_t lineNo = i + line; - - if (codeLine.stripLeft.startsWith("^") && lastLineStartedComment) - { - auto start = codeLine.indexOf("^") + 1; - assert(start != 0); - auto end = codeLine.indexOfNeither("^", start) + 1; - assert(end != 0); - auto warn = codeLine.indexOf("[warn]:"); - assert(warn != -1, "malformed line, expected `[warn]: text` after `^^^^^` part"); - auto message = codeLine[warn .. $].stripRight; - if (message.endsWith("+/", "*/")) - message = message[0 .. $ - 2].stripRight; - messages[lineNo - 1] = FoundWarning(message, start, end); - } - // Skip if no [warn] comment - else if (codeLine.indexOf("// [warn]:") != -1) - { - // Skip if there is no comment or code - immutable string codePart = codeLine.before("// "); - immutable string commentPart = codeLine.after("// "); - if (!codePart.length || !commentPart.length) - continue; - - // Get the message - messages[lineNo] = FoundWarning(commentPart); - } - } - - // Throw an assert error if any messages are not listed in the warnings - foreach (lineNo, message; messages) - { - // No warning - if (lineNo !in warnings) - { - immutable string errors = "Expected warning:\n%s\nFrom source code at (%s:?):\n%s".format(messages[lineNo], - lineNo, codeLines[lineNo - line]); - throw new AssertError(errors, file, lineNo); - } - // Different warning - else if (warnings[lineNo].msg != messages[lineNo].msg) - { - immutable string errors = "Expected warning:\n%s\nBut was:\n%s\nFrom source code at (%s:?):\n%s".format( - messages[lineNo], warnings[lineNo], lineNo, codeLines[lineNo - line]); - throw new AssertError(errors, file, lineNo); - } - - // specified column range - if ((message.startColumn || message.endColumn) - && warnings[lineNo] != message) - { - import std.algorithm : max; - import std.array : array; - import std.range : repeat; - import std.string : replace; - - const(char)[] expectedRange = ' '.repeat(max(0, cast(int)message.startColumn - 1)).array - ~ '^'.repeat(max(0, cast(int)(message.endColumn - message.startColumn))).array; - const(char)[] actualRange; - if (!warnings[lineNo].startColumn || warnings[lineNo].startColumn == warnings[lineNo].endColumn) - actualRange = "no column range defined!"; - else - actualRange = ' '.repeat(max(0, cast(int)warnings[lineNo].startColumn - 1)).array - ~ '^'.repeat(max(0, cast(int)(warnings[lineNo].endColumn - warnings[lineNo].startColumn))).array; - size_t paddingWidth = max(expectedRange.length, actualRange.length); - immutable string errors = "Wrong warning range: expected %s, but was %s\nFrom source code at (%s:?):\n%s\n%-*s <-- expected\n%-*s <-- actual".format( - [message.startColumn, message.endColumn], - [warnings[lineNo].startColumn, warnings[lineNo].endColumn], - lineNo, codeLines[lineNo - line].replace("\t", " "), - paddingWidth, expectedRange, - paddingWidth, actualRange); - throw new AssertError(errors, file, lineNo); - } - } - - // Throw an assert error if there were any warnings that were not expected - string[] unexpectedWarnings; - foreach (lineNo, warning; warnings) - { - // Unexpected warning - if (lineNo !in messages) - { - unexpectedWarnings ~= "%s\nFrom source code at (%s:?):\n%s".format(warning, - lineNo, codeLines[lineNo - line]); - } - } - if (unexpectedWarnings.length) - { - immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n"); - throw new AssertError(message, file, line); - } -} - /// EOL inside this project, for tests private static immutable fileEol = q{ }; @@ -229,27 +63,30 @@ private static immutable fileEol = q{ * available suggestion. */ void assertAutoFix(string before, string after, const StaticAnalysisConfig config, - const AutoFixFormatting formattingConfig = AutoFixFormatting(AutoFixFormatting.BraceStyle.otbs, "\t", 4, fileEol), - string file = __FILE__, size_t line = __LINE__) + string file = __FILE__, size_t line = __LINE__) { - import dparse.lexer : StringCache, Token; - import dscanner.analysis.run : parseModule; import std.algorithm : canFind, findSplit, map, sort; import std.conv : to; import std.sumtype : match; import std.typecons : tuple, Tuple; + import dscanner.analysis.autofix : improveAutoFixWhitespace; + + MessageSet rawWarnings; + auto testFileName = "test.d"; + File f = File(testFileName, "w"); + scope(exit) + { + assert(exists(testFileName)); + remove(testFileName); + } - StringCache cache = StringCache(StringCache.defaultBucketCount); - RollbackAllocator r; - const(Token)[] tokens; - const(Module) m = parseModule(file, cast(ubyte[]) before, &r, defaultErrorFormat, cache, false, tokens); + f.rawWrite(before); + f.close(); - ModuleCache moduleCache; + auto dmdModule = parseDmdModule(file, before); + rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config); - // Run the code and get any warnings - MessageSet rawWarnings = analyze("test", m, config, moduleCache, tokens, true, true, formattingConfig); string[] codeLines = before.splitLines(); - Tuple!(Message, int)[] toApply; int[] applyLines; @@ -270,8 +107,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi immutable size_t rawLine = rawWarning.endLine; if (rawLine == 0) { - stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", - rawWarning.message); + stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", rawWarning.message); continue; } @@ -285,27 +121,22 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi assert(i >= 0, "can't use negative autofix indices"); if (i >= rawWarning.autofixes.length) throw new AssertError("autofix index out of range, diagnostic only has %s autofixes (%s)." - .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), - file, rawLine + line); + .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"),file, rawLine + line); toApply ~= tuple(rawWarning, i); } else { if (rawWarning.autofixes.length != 1) throw new AssertError("diagnostic has %s autofixes (%s), but expected exactly one." - .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), - file, rawLine + line); + .format(rawWarning.autofixes.length, rawWarning.autofixes.map!"a.name"), file, rawLine + line); toApply ~= tuple(rawWarning, 0); } } } foreach (i, codeLine; codeLines) - { if (!applyLines.canFind(i) && codeLine.canFind("// fix")) - throw new AssertError("Missing expected warning for autofix on line %s" - .format(i + line), file, i + line); - } + throw new AssertError("Missing expected warning for autofix on line %s".format(i + line), file, i + line); AutoFix.CodeReplacement[] replacements; @@ -322,10 +153,7 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi string newCode = before; foreach_reverse (replacement; replacements) - { - newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText - ~ newCode[replacement.range[1] .. $]; - } + newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText ~ newCode[replacement.range[1] .. $]; if (newCode != after) { @@ -350,3 +178,107 @@ void assertAutoFix(string before, string after, const StaticAnalysisConfig confi file, line); } } + +void assertAnalyzerWarningsDMD(string code, const StaticAnalysisConfig config, bool semantic = false, + string file = __FILE__, size_t line = __LINE__) +{ + import dmd.globals : global; + + auto testFileName = "test.d"; + File f = File(testFileName, "w"); + scope(exit) + { + assert(exists(testFileName)); + remove(testFileName); + } + + f.rawWrite(code); + f.close(); + + auto dmdModule = parseDmdModule(file, code); + + if (global.errors > 0) + throw new AssertError("Failed to parse DMD module", file); + + if (semantic) + dmdModule.fullSemantic(); + + MessageSet rawWarnings = analyzeDmd(testFileName, dmdModule, getModuleName(dmdModule.md), config); + + string[] codeLines = code.splitLines(); + + // Get the warnings ordered by line + string[size_t] warnings; + foreach (rawWarning; rawWarnings[]) + { + // Skip the warning if it is on line zero + immutable size_t rawLine = rawWarning.line; + if (rawLine == 0) + { + stderr.writefln("!!! Skipping warning because it is on line zero:\n%s", + rawWarning.message); + continue; + } + + size_t warnLine = line - 1 + rawLine; + warnings[warnLine] = format("[warn]: %s", rawWarning.message); + } + + // Get all the messages from the comments in the code + string[size_t] messages; + foreach (i, codeLine; codeLines) + { + // Skip if no [warn] comment + if (codeLine.indexOf("// [warn]:") == -1) + continue; + + // Skip if there is no comment or code + immutable string codePart = codeLine.before("// "); + immutable string commentPart = codeLine.after("// "); + if (!codePart.length || !commentPart.length) + continue; + + // Get the line of this code line + size_t lineNo = i + line; + + // Get the message + messages[lineNo] = commentPart; + } + + // Throw an assert error if any messages are not listed in the warnings + foreach (lineNo, message; messages) + { + // No warning + if (lineNo !in warnings) + { + immutable string errors = "Expected warning:\n%s\nFrom source code at (%s:?):\n%s".format(messages[lineNo], + lineNo, codeLines[lineNo - line]); + throw new AssertError(errors, file, lineNo); + } + // Different warning + else if (warnings[lineNo] != messages[lineNo]) + { + immutable string errors = "Expected warning:\n%s\nBut was:\n%s\nFrom source code at (%s:?):\n%s".format( + messages[lineNo], warnings[lineNo], lineNo, codeLines[lineNo - line]); + throw new AssertError(errors, file, lineNo); + } + } + + // Throw an assert error if there were any warnings that were not expected + string[] unexpectedWarnings; + foreach (lineNo, warning; warnings) + { + // Unexpected warning + if (lineNo !in messages) + { + unexpectedWarnings ~= "%s\nFrom source code at (%s:?):\n%s".format(warning, + lineNo, codeLines[lineNo - line]); + } + } + + if (unexpectedWarnings.length) + { + immutable string message = "Unexpected warnings:\n" ~ unexpectedWarnings.join("\n"); + throw new AssertError(message, file, line); + } +} diff --git a/src/dscanner/analysis/if_constraints_indent.d b/src/dscanner/analysis/if_constraints_indent.d index 219cd711..831988bb 100644 --- a/src/dscanner/analysis/if_constraints_indent.d +++ b/src/dscanner/analysis/if_constraints_indent.d @@ -4,194 +4,132 @@ module dscanner.analysis.if_constraints_indent; -import dparse.lexer; -import dparse.ast; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; - -import std.algorithm.iteration : filter; -import std.range; +import dmd.tokens : Token, TOK; /** Checks whether all if constraints have the same indention as their declaration. */ -final class IfConstraintsIndentCheck : BaseAnalyzer +extern (C++) class IfConstraintsIndentCheck(AST) : BaseAnalyzerDmd { + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"if_constraints_indent"; - /// - this(BaseAnalyzerArguments args) - { - super(args); + private enum string KEY = "dscanner.style.if_constraints_indent"; + private enum string MSG = "If constraints should have the same indentation as the function"; - // convert tokens to a list of token starting positions per line + private Token[] tokens; - // libdparse columns start at 1 - foreach (t; tokens) - { - // pad empty positions if we skip empty token-less lines - // t.line (unsigned) may be 0 if the token is uninitialized/broken, so don't subtract from it - // equivalent to: firstSymbolAtLine.length < t.line - 1 - while (firstSymbolAtLine.length + 1 < t.line) - firstSymbolAtLine ~= Pos(1, t.index); - - // insert a new line with positions if new line is reached - // (previous while pads skipped lines) - if (firstSymbolAtLine.length < t.line) - firstSymbolAtLine ~= Pos(t.column, t.index, t.type == tok!"if"); - } - } - - override void visit(const FunctionDeclaration decl) + extern (D) this(string fileName, bool skipTests = false) { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); + super(fileName, skipTests); + lexFile(); } - override void visit(const InterfaceDeclaration decl) + private void lexFile() { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); - } + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + auto bytes = readFile(fileName) ~ '\0'; - override void visit(const ClassDeclaration decl) - { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); - } + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; - override void visit(const TemplateDeclaration decl) - { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); - } - - override void visit(const UnionDeclaration decl) - { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); - } + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); - override void visit(const StructDeclaration decl) - { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.name); - } - - override void visit(const Constructor decl) - { - if (decl.constraint !is null) - checkConstraintSpace(decl.constraint, decl.line); - } - - alias visit = ASTVisitor.visit; - -private: - - enum string KEY = "dscanner.style.if_constraints_indent"; - enum string MESSAGE = "If constraints should have the same indentation as the function"; - - Pos[] firstSymbolAtLine; - static struct Pos - { - size_t column; - size_t index; - bool isIf; - } - - /** - Check indentation of constraints - */ - void checkConstraintSpace(const Constraint constraint, const Token token) - { - checkConstraintSpace(constraint, token.line); + do + { + lexer.nextToken(); + tokens ~= lexer.token; + } + while (lexer.token.value != TOK.endOfFile); } - void checkConstraintSpace(const Constraint constraint, size_t line) + override void visit(AST.TemplateDeclaration templateDecl) { - import std.algorithm : min; - - // dscanner lines start at 1 - auto pDecl = firstSymbolAtLine[line - 1]; - - // search for constraint if (might not be on the same line as the expression) - auto r = firstSymbolAtLine[line .. constraint.expression.line].retro.filter!(s => s.isIf); - - auto if_ = constraint.tokens.findTokenForDisplay(tok!"if")[0]; - - // no hit = constraint is on the same line - if (r.empty) - addErrorMessage(if_, KEY, MESSAGE); - else if (pDecl.column != r.front.column) - addErrorMessage([min(if_.index, pDecl.index), if_.index + 2], if_.line, [min(if_.column, pDecl.column), if_.column + 2], KEY, MESSAGE); + import std.array : array; + import std.algorithm : filter; + import std.range : front, retro; + + super.visit(templateDecl); + + if (templateDecl.constraint is null || templateDecl.members is null) + return; + + auto firstTemplateToken = tokens.filter!(t => t.loc.linnum == templateDecl.loc.linnum) + .filter!(t => t.value != TOK.whitespace) + .front; + uint templateLine = firstTemplateToken.loc.linnum; + uint templateCol = firstTemplateToken.loc.charnum; + + auto constraintToken = tokens.filter!(t => t.loc.fileOffset <= templateDecl.constraint.loc.fileOffset) + .array() + .retro() + .filter!(t => t.value == TOK.if_) + .front; + uint constraintLine = constraintToken.loc.linnum; + uint constraintCol = constraintToken.loc.charnum; + + if (templateLine == constraintLine || templateCol != constraintCol) + addErrorMessage(cast(ulong) constraintLine, cast(ulong) constraintCol, KEY, MSG); } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.format : format; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.if_constraints_indent = Check.enabled; + enum MSG = "If constraints should have the same indentation as the function"; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo(R)(R r) if (R == null) {} -// note: since we are using tabs, the ^ look a bit off here. Use 1-wide tab stops to view tests. void foo(R)(R r) - if (R == null) /+ -^^^ [warn]: %s +/ + if (R == null) // [warn]: %s {} - }c.format( - IfConstraintsIndentCheck.MESSAGE, - ), sac); + }c.format(MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo(R)(R r) if (R == null) {} void foo(R)(R r) -if (R == null) /+ -^^ [warn]: %s +/ +if (R == null) // [warn]: %s {} void foo(R)(R r) - if (R == null) /+ - ^^^ [warn]: %s +/ + if (R == null) // [warn]: %s {} - }c.format( - IfConstraintsIndentCheck.MESSAGE, - IfConstraintsIndentCheck.MESSAGE, - ), sac); + }c.format(MSG, MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct Foo(R) if (R == null) {} struct Foo(R) -if (R == null) /+ -^^ [warn]: %s +/ +if (R == null) // [warn]: %s {} struct Foo(R) - if (R == null) /+ - ^^^ [warn]: %s +/ + if (R == null) // [warn]: %s {} - }c.format( - IfConstraintsIndentCheck.MESSAGE, - IfConstraintsIndentCheck.MESSAGE, - ), sac); + }c.format(MSG, MSG), sac); // test example from Phobos - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ Num abs(Num)(Num x) @safe pure nothrow if (is(typeof(Num.init >= 0)) && is(typeof(-Num.init)) && !(is(Num* : const(ifloat*)) || is(Num* : const(idouble*)) @@ -205,7 +143,7 @@ if (is(typeof(Num.init >= 0)) && is(typeof(-Num.init)) && }, sac); // weird constraint formatting - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct Foo(R) if (R == null) @@ -217,8 +155,7 @@ if (is(typeof(Num.init >= 0)) && is(typeof(-Num.init)) && {} struct Foo(R) -if /+ -^^ [warn]: %s +/ +if // [warn]: %s (R == null) {} @@ -234,39 +171,61 @@ if /+ {} struct Foo(R) - if ( /+ - ^^^ [warn]: %s +/ + if ( // [warn]: %s R == null ) {} - }c.format( - IfConstraintsIndentCheck.MESSAGE, - IfConstraintsIndentCheck.MESSAGE, - ), sac); + }c.format(MSG, MSG), sac); // constraint on the same line - assertAnalyzerWarnings(q{ - struct CRC(uint N, ulong P) if (N == 32 || N == 64) /+ - ^^ [warn]: %s +/ + assertAnalyzerWarningsDMD(q{ + struct CRC(uint N, ulong P) if (N == 32 || N == 64) // [warn]: %s {} - }c.format( - IfConstraintsIndentCheck.MESSAGE, - ), sac); + }c.format(MSG), sac); - stderr.writeln("Unittest for IfConstraintsIndentCheck passed."); + assertAnalyzerWarningsDMD(q{ +private template sharedToString(alias field) +if (is(typeof(field) == shared)) +{ + static immutable sharedToString = typeof(field).stringof; } + }c, sac); -@("issue #829") -unittest + assertAnalyzerWarningsDMD(q{ +private union EndianSwapper(T) +if (canSwapEndianness!T) { - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; - import std.format : format; - import std.stdio : stderr; + T value; +} + }c, sac); - StaticAnalysisConfig sac = disabledConfig(); - sac.if_constraints_indent = Check.enabled; + assertAnalyzerWarningsDMD(q{ +void test(alias matchFn)() +{ + auto baz(Cap)(Cap m) + if (is(Cap == Captures!(Cap.String))) + { + return toUpper(m.hit); + } +} + }c, sac); - assertAnalyzerWarnings(`void foo() { - '' -}`, sac); + assertAnalyzerWarningsDMD(q{ +ElementType!(A) pop (A) (ref A a) +if (isDynamicArray!(A) && !isNarrowString!(A) && isMutable!(A) && !is(A == void[])) +{ + auto e = a.back; + a.popBack(); + return e; +} + }c, sac); + + assertAnalyzerWarningsDMD(q{ + template HMAC(H) + if (isDigest!H && hasBlockSize!H) + { + alias HMAC = HMAC!(H, H.blockSize); + } + }, sac); + + stderr.writeln("Unittest for IfConstraintsIndentCheck passed."); } diff --git a/src/dscanner/analysis/if_statements.d b/src/dscanner/analysis/if_statements.d deleted file mode 100644 index 532ab4ad..00000000 --- a/src/dscanner/analysis/if_statements.d +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright Brian Schott (Hackerpilot) 2015. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) -module dscanner.analysis.if_statements; - -import dparse.ast; -import dparse.lexer; -import dparse.formatter; -import dscanner.analysis.base; -import dsymbol.scope_ : Scope; -import std.typecons : Rebindable, rebindable; - -final class IfStatementCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - mixin AnalyzerInfo!"redundant_if_check"; - - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const IfStatement ifStatement) - { - import std.string : format; - import std.algorithm : sort, countUntil; - import std.array : appender; - - ++depth; - - if (ifStatement.condition.expression.items.length == 1 - && (cast(AndAndExpression) ifStatement.condition.expression.items[0]) is null) - { - redundancyCheck(ifStatement.condition.expression, - ifStatement.condition.expression.line, ifStatement.condition.expression.column); - } - inIfExpresson = true; - ifStatement.condition.expression.accept(this); - inIfExpresson = false; - ifStatement.thenStatement.accept(this); - if (expressions.length) - expressions = expressions[0 .. expressions.countUntil!(a => a.depth + 1 >= depth)]; - if (ifStatement.elseStatement) - ifStatement.elseStatement.accept(this); - --depth; - } - - override void visit(const AndAndExpression andAndExpression) - { - if (inIfExpresson) - { - redundancyCheck(andAndExpression, andAndExpression.line, andAndExpression.column); - redundancyCheck(andAndExpression.left, andAndExpression.line, andAndExpression.column); - redundancyCheck(andAndExpression.right, andAndExpression.line, - andAndExpression.column); - } - andAndExpression.accept(this); - } - - override void visit(const OrOrExpression orOrExpression) - { - // intentionally does nothing - } - -private: - invariant - { - assert(depth >= 0); - } - - void redundancyCheck(const ExpressionNode expression, size_t line, size_t column) - { - import std.string : format; - import std.array : appender; - import std.algorithm : sort; - - if (expression is null) - return; - auto app = appender!string(); - dparse.formatter.format(app, expression); - immutable size_t prevLocation = alreadyChecked(app.data, line, column); - if (prevLocation != size_t.max) - { - addErrorMessage(expressions[prevLocation].astNode, KEY, "Expression %s is true: already checked on line %d.".format( - expressions[prevLocation].formatted, expressions[prevLocation].line)); - } - else - { - expressions ~= ExpressionInfo(app.data, line, column, depth, (cast(const BaseNode) expression).rebindable); - sort(expressions); - } - } - - size_t alreadyChecked(string expressionText, size_t line, size_t column) - { - foreach (i, ref info; expressions) - { - if (info.line == line && info.column == column) - continue; - if (info.formatted == expressionText) - return i; - } - return size_t.max; - } - - bool inIfExpresson; - int depth; - ExpressionInfo[] expressions; - enum string KEY = "dscanner.if_statement"; -} - -private struct ExpressionInfo -{ - int opCmp(ref const ExpressionInfo other) const nothrow - { - if (line < other.line || (line == other.line && column < other.column)) - return 1; - if (line == other.line && column == other.column) - return 0; - return -1; - } - - string formatted; - size_t line; - size_t column; - int depth; - Rebindable!(const BaseNode) astNode; -} diff --git a/src/dscanner/analysis/ifelsesame.d b/src/dscanner/analysis/ifelsesame.d index ca8a3ee8..ae9a6e7b 100644 --- a/src/dscanner/analysis/ifelsesame.d +++ b/src/dscanner/analysis/ifelsesame.d @@ -5,117 +5,162 @@ module dscanner.analysis.ifelsesame; -import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; +import dmd.hdrgen : toChars; +import dmd.tokens : EXP; +import std.conv : to; +import std.string : format; +import std.typecons : Tuple, tuple; /** * Checks for duplicated code in conditional and logical expressions. * $(UL * $(LI If statements whose "then" block is the same as the "else" block) * $(LI || and && expressions where the left and right are the same) - * $(LI == expressions where the left and right are the same) + * $(LI == and != expressions where the left and right are the same) + * $(LI >, <, >=, and <= expressions where the left and right are the same) + * $(LI Assignments where the left and right are the same) * ) */ -final class IfElseSameCheck : BaseAnalyzer +extern (C++) class IfElseSameCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"if_else_same_check"; - this(BaseAnalyzerArguments args) + private enum IF_KEY = "dscanner.bugs.if_else_same"; + private enum IF_MESSAGE = "'Else' branch is identical to 'Then' branch."; + + private enum LOGICAL_EXP_KEY = "dscanner.bugs.logic_operator_operands"; + private enum LOGICAL_EXP_MESSAGE = "Left side of logical %s is identical to right side."; + + private enum ASSIGN_KEY = "dscanner.bugs.self_assignment"; + private enum ASSIGN_MESSAGE = "Left side of assignment operation is identical to the right side."; + + private bool inAssignment = false; + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const IfStatement ifStatement) + override void visit(AST.IfStatement ifStatement) { - if (ifStatement.thenStatement && (ifStatement.thenStatement == ifStatement.elseStatement)) + super.visit(ifStatement); + + if (ifStatement.ifbody is null || ifStatement.elsebody is null) + return; + + auto thenBody = to!string(toChars(ifStatement.ifbody)); + auto elseBody = to!string(toChars(ifStatement.elsebody)); + + if (thenBody == elseBody) { - const(Token)[] tokens = ifStatement.elseStatement.tokens; - // extend 1 past, so we include the `else` token - tokens = (tokens.ptr - 1)[0 .. tokens.length + 1]; - addErrorMessage(tokens, - IF_ELSE_SAME_KEY, "'Else' branch is identical to 'Then' branch."); + auto lineNum = cast(ulong) ifStatement.loc.linnum; + auto charNum = cast(ulong) ifStatement.loc.charnum; + addErrorMessage(lineNum, charNum, IF_KEY, IF_MESSAGE); } - ifStatement.accept(this); } - override void visit(const AssignExpression assignExpression) + override void visit(AST.AssignExp assignExp) { - auto e = cast(const AssignExpression) assignExpression.expression; - if (e !is null && assignExpression.operator == tok!"=" - && e.ternaryExpression == assignExpression.ternaryExpression) - { - addErrorMessage(assignExpression, SELF_ASSIGNMENT_KEY, - "Left side of assignment operatior is identical to the right side."); - } - assignExpression.accept(this); + bool oldInAssignment = inAssignment; + inAssignment = true; + super.visit(assignExp); + inAssignment = oldInAssignment; } - override void visit(const AndAndExpression andAndExpression) + override void visit(AST.CondExp condExp) { - if (andAndExpression.left !is null && andAndExpression.right !is null - && andAndExpression.left == andAndExpression.right) - { - addErrorMessage(andAndExpression.right, - LOGIC_OPERATOR_OPERANDS_KEY, - "Left side of logical and is identical to right side."); - } - andAndExpression.accept(this); + super.visit(condExp); + if (inAssignment) + handleBinaryExpression(condExp); } - override void visit(const OrOrExpression orOrExpression) + override void visit(AST.LogicalExp logicalExpr) { - if (orOrExpression.left !is null && orOrExpression.right !is null - && orOrExpression.left == orOrExpression.right) + super.visit(logicalExpr); + handleBinaryExpression(logicalExpr); + } + + private void handleBinaryExpression(AST.BinExp expr) + { + auto expr1 = to!string(toChars(expr.e1)); + auto expr2 = to!string(toChars(expr.e2)); + + if (expr1 == expr2) { - addErrorMessage(orOrExpression.right, - LOGIC_OPERATOR_OPERANDS_KEY, - "Left side of logical or is identical to right side."); + auto lineNum = cast(ulong) expr.loc.linnum; + auto charNum = cast(ulong) expr.loc.charnum; + auto errorInfo = getErrorInfo(expr.op); + addErrorMessage(lineNum, charNum, errorInfo[0], errorInfo[1]); } - orOrExpression.accept(this); } -private: + private extern (D) Tuple!(string, string) getErrorInfo(EXP op) + { + switch (op) + { + case EXP.orOr: + return tuple(LOGICAL_EXP_KEY, LOGICAL_EXP_MESSAGE.format("or")); + case EXP.andAnd: + return tuple(LOGICAL_EXP_KEY, LOGICAL_EXP_MESSAGE.format("and")); + case EXP.question: + return tuple(ASSIGN_KEY, ASSIGN_MESSAGE); + default: + assert(0); + } - enum string IF_ELSE_SAME_KEY = "dscanner.bugs.if_else_same"; - enum string SELF_ASSIGNMENT_KEY = "dscanner.bugs.self_assignment"; - enum string LOGIC_OPERATOR_OPERANDS_KEY = "dscanner.bugs.logic_operator_operands"; + } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.if_else_same_check = Check.enabled; - assertAnalyzerWarnings(q{ - void testSizeT() + + assertAnalyzerWarningsDMD(q{ + void testThenElseSame() { string person = "unknown"; - if (person == "unknown") - person = "bobrick"; /* same */ + if (person == "unknown") // [warn]: 'Else' branch is identical to 'Then' branch. + person = "bobrick"; else - person = "bobrick"; /* same */ /+ -^^^^^^^^^^^^^^^^^^^^^^^ [warn]: 'Else' branch is identical to 'Then' branch. +/ - // 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 - // look at the tests using 1-wide tab width for accurate visuals. + person = "bobrick"; - if (person == "unknown") // ok - person = "ricky"; // not same + if (person == "unknown") + person = "ricky"; else - person = "bobby"; // not same + person = "bobby"; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testLogicalExp() + { + int a = 5, b = 5; + if (a == b || a == b) // [warn]: Left side of logical or is identical to right side. + a = 6; + if (a == b && a == b) // [warn]: Left side of logical and is identical to right side. + a = 6; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testAssignExp() + { + int a = 5, b = 5; + a = b > 5 ? b : b; // [warn]: Left side of assignment operation is identical to the right side. } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo() { - if (auto stuff = call()) + if (auto stuff = call()) {} } }c, sac); diff --git a/src/dscanner/analysis/imports_sortedness.d b/src/dscanner/analysis/imports_sortedness.d index e32a26cd..6238215d 100644 --- a/src/dscanner/analysis/imports_sortedness.d +++ b/src/dscanner/analysis/imports_sortedness.d @@ -5,106 +5,100 @@ module dscanner.analysis.imports_sortedness; import dscanner.analysis.base; -import dparse.lexer; -import dparse.ast; - -import std.stdio; /** * Checks the sortedness of module imports */ -final class ImportSortednessCheck : BaseAnalyzer +extern(C++) class ImportSortednessCheck(AST) : BaseAnalyzerDmd { enum string KEY = "dscanner.style.imports_sortedness"; enum string MESSAGE = "The imports are not sorted in alphabetical order"; mixin AnalyzerInfo!"imports_sortedness"; + alias visit = BaseAnalyzerDmd.visit; + // alias visit = BaseAnalyzerDmd!AST.visit; /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); } - mixin ScopedVisit!Module; - mixin ScopedVisit!Statement; - mixin ScopedVisit!BlockStatement; - mixin ScopedVisit!StructBody; - mixin ScopedVisit!IfStatement; - mixin ScopedVisit!TemplateDeclaration; - mixin ScopedVisit!ConditionalDeclaration; + mixin ScopedVisit!(AST.StructDeclaration); + mixin ScopedVisit!(AST.FuncDeclaration); + mixin ScopedVisit!(AST.InterfaceDeclaration); + mixin ScopedVisit!(AST.UnionDeclaration); + mixin ScopedVisit!(AST.TemplateDeclaration); + mixin ScopedVisit!(AST.IfStatement); + mixin ScopedVisit!(AST.WhileStatement); + mixin ScopedVisit!(AST.ForStatement); + mixin ScopedVisit!(AST.ForeachStatement); + mixin ScopedVisit!(AST.ScopeStatement); + mixin ScopedVisit!(AST.ConditionalDeclaration); + - override void visit(const VariableDeclaration id) + override void visit(AST.VarDeclaration vd) { imports[level] = []; } - override void visit(const ImportDeclaration id) + override void visit(AST.Import i) { - import std.algorithm.iteration : map; + import std.algorithm : map; import std.array : join; - import std.string : strip; + import std.conv : to; - if (id.importBindings is null || id.importBindings.importBinds.length == 0) - { - bool suppress; - foreach (singleImport; id.singleImports) - { - string importModuleName = singleImport.identifierChain.identifiers.map!`a.text`.join("."); - addImport(importModuleName, singleImport, null, suppress); - } - } + string importModuleName = i.packages.map!(a => a.toString().dup).join("."); + + if (importModuleName != "") + importModuleName ~= "." ~ i.id.toString(); else - { - string importModuleName = id.importBindings.singleImport.identifierChain.identifiers.map!`a.text`.join("."); + importModuleName ~= i.id.toString(); - bool suppress; - foreach (importBind; id.importBindings.importBinds) + if (i.names.length) + { + foreach (name; i.names) { - addImport(importModuleName ~ "-" ~ importBind.left.text, importBind, id.importBindings.singleImport, suppress); + string aux = to!string(importModuleName ~ "-" ~ name.toString()); + addImport(aux, i); } } + else addImport(importModuleName, i); } - alias visit = BaseAnalyzer.visit; - private: - + enum maxDepth = 20; int level; string[][int] imports; + bool[maxDepth] levelAvailable; template ScopedVisit(NodeType) { - override void visit(const NodeType n) + override void visit(NodeType n) { + if (level >= maxDepth) + return; + + imports[level] = []; imports[++level] = []; - n.accept(this); + levelAvailable[level] = true; + super.visit(n); level--; } } - void addImport(string importModuleName, const BaseNode range, const BaseNode parent, ref bool suppress) + extern(D) void addImport(string importModuleName, AST.Import i) { - import std.algorithm : findSplit; - import std.string : indexOf; import std.uni : sicmp; - if (imports[level].length > 0 && imports[level][$ -1].sicmp(importModuleName) > 0) + if (!levelAvailable[level]) { - if (parent !is null) - { - auto parentEnd = importModuleName.indexOf("-"); - if (parentEnd != -1 && imports[level][$ -1].findSplit("-")[0].sicmp(importModuleName) > 0) - { - // mark module name as broken, not selected symbols, since it's the module name is not belonging here - if (!suppress) - addErrorMessage(parent, KEY, MESSAGE); - suppress = true; - return; - } - } - if (!suppress) - addErrorMessage(range, KEY, MESSAGE); - suppress = true; + imports[level] = []; + levelAvailable[level] = true; + } + + if (imports[level].length > 0 && imports[level][$ - 1].sicmp(importModuleName) > 0) + { + addErrorMessage(cast(ulong) i.loc.linnum, cast(ulong) i.loc.charnum, KEY, MESSAGE); } else { @@ -116,9 +110,8 @@ private: unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.imports_sortedness = Check.enabled; @@ -130,62 +123,30 @@ unittest assertAnalyzerWarnings(q{ import foo.bar; - import bar.foo; /+ - ^^^^^^^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ), sac); + import bar.foo; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ import c; import c.b; - import c.a; /+ - ^^^ [warn]: %s +/ + import c.a; // [warn]: The imports are not sorted in alphabetical order import d.a; - import d; /+ - ^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import d; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ - unittest - { - import a.b, a.c, a.d; - } - unittest - { - import a.b, a.d, a.c; /+ - ^^^ [warn]: %s +/ - } - unittest - { - import a.c, a.b, a.c; /+ - ^^^ [warn]: %s +/ - } - unittest - { - import foo.bar, bar.foo; /+ - ^^^^^^^ [warn]: %s +/ - } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import a.b, a.c, a.d; + import a.b, a.d, a.c; // [warn]: The imports are not sorted in alphabetical order + import a.c, a.b, a.c; // [warn]: The imports are not sorted in alphabetical order + import foo.bar, bar.foo; // [warn]: The imports are not sorted in alphabetical order + }c, sac); // multiple items out of order assertAnalyzerWarnings(q{ import foo.bar; - import bar.foo; /+ - ^^^^^^^ [warn]: %s +/ - import bar.bar.foo; /+ - ^^^^^^^^^^^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import bar.foo; // [warn]: The imports are not sorted in alphabetical order + import bar.bar.foo; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ import test : bar; @@ -195,47 +156,28 @@ unittest // selective imports assertAnalyzerWarnings(q{ import test : foo; - import test : bar; /+ - ^^^ [warn]: %s +/ - import before : zzz; /+ - ^^^^^^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import test : bar; // [warn]: The imports are not sorted in alphabetical order + }c, sac); // selective imports assertAnalyzerWarnings(q{ - import test : foo, bar; /+ - ^^^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ), sac); + import test : foo, bar; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ import b; import c : foo; - import c : bar; /+ - ^^^ [warn]: %s +/ - import a; /+ - ^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import c : bar; // [warn]: The imports are not sorted in alphabetical order + import a; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ import c; import c : bar; import d : bar; - import d; /+ - ^ [warn]: %s +/ - import a : bar; /+ - ^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import d; // [warn]: The imports are not sorted in alphabetical order + import a : bar; // [warn]: The imports are not sorted in alphabetical order + }c, sac); assertAnalyzerWarnings(q{ import t0; @@ -245,25 +187,18 @@ unittest assertAnalyzerWarnings(q{ import t1 : a, b = foo; - import t1 : b, a = foo; /+ - ^^^^^^^ [warn]: %s +/ - import t0 : a, b = foo; /+ - ^^ [warn]: %s +/ - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + import t1 : b, a = foo; // [warn]: The imports are not sorted in alphabetical order + import t0 : a, b = foo; // [warn]: The imports are not sorted in alphabetical order + }c, sac); // local imports in functions assertAnalyzerWarnings(q{ import t2; - import t1; /+ - ^^ [warn]: %s +/ + import t1; // [warn]: The imports are not sorted in alphabetical order void foo() { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } void bar() @@ -271,26 +206,20 @@ unittest import f1; import f2; } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + }c, sac); // local imports in scopes assertAnalyzerWarnings(q{ import t2; - import t1; /+ - ^^ [warn]: %s +/ + import t1; // [warn]: The imports are not sorted in alphabetical order void foo() { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } { @@ -299,27 +228,20 @@ unittest import f3; } } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + }c, sac); // local imports in functions assertAnalyzerWarnings(q{ import t2; - import t1; /+ - ^^ [warn]: %s +/ + import t1; // [warn]: The imports are not sorted in alphabetical order void foo() { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; while (true) { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } for (;;) { @@ -329,66 +251,47 @@ unittest } foreach (el; arr) { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + }c, sac); // nested scopes assertAnalyzerWarnings(q{ import t2; - import t1; /+ - ^^ [warn]: %s +/ + import t1; // [warn]: The imports are not sorted in alphabetical order void foo() { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } } } } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + }c, sac); // local imports in functions assertAnalyzerWarnings(q{ import t2; - import t1; /+ - ^^ [warn]: %s +/ + import t1; // [warn]: The imports are not sorted in alphabetical order struct foo() { import f2; - import f1; /+ - ^^ [warn]: %s +/ + import f1; // [warn]: The imports are not sorted in alphabetical order import f3; } class bar() @@ -396,10 +299,7 @@ unittest import f1; import f2; } - }c.format( - ImportSortednessCheck.MESSAGE, - ImportSortednessCheck.MESSAGE, - ), sac); + }c, sac); // issue 422 - sorted imports with : assertAnalyzerWarnings(q{ @@ -448,4 +348,4 @@ unittest }, sac); stderr.writeln("Unittest for ImportSortednessCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/incorrect_infinite_range.d b/src/dscanner/analysis/incorrect_infinite_range.d index 8356e4bc..843e4863 100644 --- a/src/dscanner/analysis/incorrect_infinite_range.d +++ b/src/dscanner/analysis/incorrect_infinite_range.d @@ -7,93 +7,89 @@ module dscanner.analysis.incorrect_infinite_range; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; - -import std.typecons : Rebindable; /** * Checks for incorrect infinite range definitions */ -final class IncorrectInfiniteRangeCheck : BaseAnalyzer +extern(C++) class IncorrectInfiniteRangeCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; + // alias visit = BaseAnalyzerDmd!AST.visit; + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"incorrect_infinite_range_check"; /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); + super(fileName); } - override void visit(const StructBody structBody) + override void visit(AST.StructDeclaration sd) { - inStruct++; - structBody.accept(this); - inStruct--; + inAggregate++; + super.visit(sd); + inAggregate--; } - override void visit(const FunctionDeclaration fd) + override void visit(AST.ClassDeclaration cd) { - if (inStruct > 0 && fd.name.text == "empty") - { - auto old = parentFunc; - parentFunc = fd; - fd.accept(this); - parentFunc = old; - } + inAggregate++; + super.visit(cd); + inAggregate--; } - override void visit(const FunctionBody fb) + override void visit(AST.FuncDeclaration fd) { - if (fb.specifiedFunctionBody && fb.specifiedFunctionBody.blockStatement !is null) - visit(fb.specifiedFunctionBody.blockStatement); - else if (fb.shortenedFunctionBody && fb.shortenedFunctionBody.expression !is null) - visitReturnExpression(fb.shortenedFunctionBody.expression); - } + import dmd.astenums : Tbool; - override void visit(const BlockStatement bs) - { - if (bs.declarationsAndStatements is null) - return; - if (bs.declarationsAndStatements.declarationsAndStatements is null) - return; - if (bs.declarationsAndStatements.declarationsAndStatements.length != 1) + if (!inAggregate) return; - visit(bs.declarationsAndStatements); - } - override void visit(const ReturnStatement rs) - { - if (inStruct == 0 || parentFunc == null) // not within a struct yet + if (!fd.ident || fd.ident.toString() != "empty") return; - visitReturnExpression(rs.expression); - } - void visitReturnExpression(const Expression expression) - { - if (!expression || expression.items.length != 1) - return; - UnaryExpression unary = cast(UnaryExpression) expression.items[0]; - if (unary is null) - return; - if (unary.primaryExpression is null) + AST.TypeFunction tf = fd.type.isTypeFunction(); + + if (!tf || !tf.next || !tf.next.ty) return; - if (unary.primaryExpression.primary != tok!"false") + + AST.ReturnStatement rs = fd.fbody ? fd.fbody.isReturnStatement() : null; + + if (rs) + { + AST.IntegerExp ie = cast(AST.IntegerExp) rs.exp; + + if (ie && ie.getInteger() == 0) + addErrorMessage(cast(ulong) fd.loc.linnum, cast(ulong) fd.loc.charnum, KEY, + "Use `enum bool empty = false;` to define an infinite range."); + } + + AST.CompoundStatement cs = fd.fbody ? fd.fbody.isCompoundStatement() : null; + + if (!cs || (*cs.statements).length == 0) return; - addErrorMessage(parentFunc.get, KEY, MESSAGE); + + if (auto rs1 = (*cs.statements)[0].isReturnStatement()) + { + AST.IntegerExp ie = cast(AST.IntegerExp) rs1.exp; + + if (ie && ie.getInteger() == 0) + addErrorMessage(cast(ulong) fd.loc.linnum, cast(ulong) fd.loc.charnum, KEY, + "Use `enum bool empty = false;` to define an infinite range."); + } + + super.visit(fd); } - override void visit(const Unittest u) + override void visit(AST.UnitTestDeclaration ud) { + } private: - uint inStruct; + uint inAggregate; enum string KEY = "dscanner.suspicious.incorrect_infinite_range"; enum string MESSAGE = "Use `enum bool empty = false;` to define an infinite range."; - Rebindable!(const FunctionDeclaration) parentFunc; } unittest @@ -104,14 +100,12 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.incorrect_infinite_range_check = Check.enabled; - assertAnalyzerWarnings(q{struct InfiniteRange + assertAnalyzerWarningsDMD(q{struct InfiniteRange { - bool empty() + bool empty() // [warn]: Use `enum bool empty = false;` to define an infinite range. { return false; - } /+ -^^ [warn]: %1$s+/ - // TODO: test for multiline issues like this + } bool stuff() { @@ -132,8 +126,7 @@ unittest struct InfiniteRange { - bool empty() => false; /+ - ^^^^^^^^^^^^^^^^^^^^^^ [warn]: %1$s +/ + bool empty() => false; // [warn]: Use `enum bool empty = false;` to define an infinite range. bool stuff() => false; unittest { @@ -148,11 +141,9 @@ struct InfiniteRange } bool empty() { return false; } -class C { bool empty() { return false; } } /+ - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [warn]: %1$s +/ +class C { bool empty() { return false; } } // [warn]: Use `enum bool empty = false;` to define an infinite range. -}c - .format(IncorrectInfiniteRangeCheck.MESSAGE), sac); +}c, sac); } // test for https://github.com/dlang-community/D-Scanner/issues/656 @@ -173,7 +164,7 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.incorrect_infinite_range_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ enum isAllZeroBits = () { if (true) @@ -183,4 +174,4 @@ unittest }(); }, sac); stderr.writeln("Unittest for IncorrectInfiniteRangeCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/label_var_same_name_check.d b/src/dscanner/analysis/label_var_same_name_check.d index b915006b..3fc1d893 100644 --- a/src/dscanner/analysis/label_var_same_name_check.d +++ b/src/dscanner/analysis/label_var_same_name_check.d @@ -4,174 +4,303 @@ module dscanner.analysis.label_var_same_name_check; -import dparse.ast; -import dparse.lexer; -import dsymbol.scope_ : Scope; import dscanner.analysis.base; -import dscanner.analysis.helpers; +import dmd.cond : Include; +import std.conv : to; /** * Checks for labels and variables that have the same name. */ -final class LabelVarNameCheck : ScopedBaseAnalyzer +extern (C++) class LabelVarNameCheck(AST) : BaseAnalyzerDmd { mixin AnalyzerInfo!"label_var_same_name_check"; - - this(BaseAnalyzerArguments args) + alias visit = BaseAnalyzerDmd.visit; + + mixin ScopedVisit!(AST.Module); + mixin ScopedVisit!(AST.IfStatement); + mixin ScopedVisit!(AST.WithStatement); + mixin ScopedVisit!(AST.WhileStatement); + mixin ScopedVisit!(AST.DoStatement); + mixin ScopedVisit!(AST.ForStatement); + mixin ScopedVisit!(AST.CaseStatement); + mixin ScopedVisit!(AST.ForeachStatement); + mixin ScopedVisit!(AST.ForeachRangeStatement); + mixin ScopedVisit!(AST.ScopeStatement); + mixin ScopedVisit!(AST.FuncAliasDeclaration); + mixin ScopedVisit!(AST.CtorDeclaration); + mixin ScopedVisit!(AST.DtorDeclaration); + mixin ScopedVisit!(AST.InvariantDeclaration); + + mixin FunctionVisit!(AST.FuncDeclaration); + mixin FunctionVisit!(AST.TemplateDeclaration); + mixin FunctionVisit!(AST.FuncLiteralDeclaration); + + mixin AggregateVisit!(AST.ClassDeclaration); + mixin AggregateVisit!(AST.StructDeclaration); + mixin AggregateVisit!(AST.InterfaceDeclaration); + mixin AggregateVisit!(AST.UnionDeclaration); + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName); } - mixin AggregateVisit!ClassDeclaration; - mixin AggregateVisit!StructDeclaration; - mixin AggregateVisit!InterfaceDeclaration; - mixin AggregateVisit!UnionDeclaration; + override void visit(AST.VarDeclaration vd) + { + import dmd.astenums : STC; + + if (!(vd.storage_class & STC.scope_) && !isInLocalFunction) + { + auto thing = Thing(to!string(vd.ident.toChars()), vd.loc.linnum, vd.loc.charnum); + duplicateCheck(thing, false, conditionalDepth > 0); + } + + super.visit(vd); + } - override void visit(const VariableDeclaration var) + override void visit(AST.LabelStatement ls) { - foreach (dec; var.declarators) - duplicateCheck(dec.name, false, conditionalDepth > 0); + auto thing = Thing(to!string(ls.ident.toChars()), ls.loc.linnum, ls.loc.charnum); + duplicateCheck(thing, true, conditionalDepth > 0); + super.visit(ls); } - override void visit(const LabeledStatement labeledStatement) + override void visit(AST.ConditionalDeclaration conditionalDeclaration) { - duplicateCheck(labeledStatement.identifier, true, conditionalDepth > 0); - if (labeledStatement.declarationOrStatement !is null) - labeledStatement.declarationOrStatement.accept(this); + import dmd.root.array : peekSlice; + + conditionalDeclaration.condition.accept(this); + + if (conditionalDeclaration.condition.isVersionCondition()) + ++conditionalDepth; + + if (conditionalDeclaration.elsedecl || conditionalDeclaration.condition.inc != Include.yes) + pushScope(); + + foreach (decl; conditionalDeclaration.decl.peekSlice()) + decl.accept(this); + + if (conditionalDeclaration.elsedecl || conditionalDeclaration.condition.inc != Include.yes) + popScope(); + + if (conditionalDeclaration.elsedecl) + foreach (decl; conditionalDeclaration.elsedecl.peekSlice()) + decl.accept(this); + + if (conditionalDeclaration.condition.isVersionCondition()) + --conditionalDepth; } - override void visit(const ConditionalDeclaration condition) + override void visit(AST.ConditionalStatement conditionalStatement) { - if (condition.falseDeclarations.length > 0) + conditionalStatement.condition.accept(this); + + if (conditionalStatement.condition.isVersionCondition) ++conditionalDepth; - condition.accept(this); - if (condition.falseDeclarations.length > 0) + + if (conditionalStatement.ifbody) + { + if (conditionalStatement.elsebody) + pushScope(); + + conditionalStatement.ifbody.accept(this); + + if (conditionalStatement.elsebody) + popScope(); + } + + if (conditionalStatement.elsebody) + { + if (conditionalStatement.condition.inc == Include.no) + pushScope(); + + conditionalStatement.elsebody.accept(this); + + if (conditionalStatement.condition.inc == Include.no) + popScope(); + } + + if (conditionalStatement.condition.isVersionCondition) --conditionalDepth; } - override void visit(const VersionCondition condition) + override void visit(AST.AnonDeclaration ad) { - ++conditionalDepth; - condition.accept(this); - --conditionalDepth; + pushScope(); + pushAggregateName("", ad.loc.linnum, ad.loc.charnum); + super.visit(ad); + popScope(); + popAggregateName(); } - alias visit = ScopedBaseAnalyzer.visit; + override void visit(AST.UnitTestDeclaration unitTestDecl) + { + if (skipTests) + return; + + auto oldIsInFunction = isInFunction; + auto oldIsInLocalFunction = isInLocalFunction; -private: + pushScope(); + + if (isInFunction) + isInLocalFunction = true; + else + isInFunction = true; + super.visit(unitTestDecl); + popScope(); + + isInFunction = oldIsInFunction; + isInLocalFunction = oldIsInLocalFunction; + } + +private: + extern (D) Thing[string][] stack; + int conditionalDepth; + extern (D) Thing[] parentAggregates; + extern (D) string parentAggregateText; + bool isInFunction; + bool isInLocalFunction; enum string KEY = "dscanner.suspicious.label_var_same_name"; - Thing[string][] stack; + template FunctionVisit(NodeType) + { + override void visit(NodeType n) + { + auto oldIsInFunction = isInFunction; + auto oldIsInLocalFunction = isInLocalFunction; + + pushScope(); + + if (isInFunction) + isInLocalFunction = true; + else + isInFunction = true; + + super.visit(n); + popScope(); + + isInFunction = oldIsInFunction; + isInLocalFunction = oldIsInLocalFunction; + } + } template AggregateVisit(NodeType) { - override void visit(const NodeType n) + override void visit(NodeType n) { - pushAggregateName(n.name); - n.accept(this); + pushScope(); + pushAggregateName(to!string(n.ident.toString()), n.loc.linnum, n.loc.charnum); + super.visit(n); + popScope(); popAggregateName(); } } - void duplicateCheck(const Token name, bool fromLabel, bool isConditional) + template ScopedVisit(NodeType) + { + override void visit(NodeType n) + { + pushScope(); + super.visit(n); + popScope(); + } + } + + extern (D) void duplicateCheck(const Thing id, bool fromLabel, bool isConditional) { - import std.conv : to; import std.range : retro; size_t i; foreach (s; retro(stack)) { - string fqn = parentAggregateText ~ name.text; + string fqn = parentAggregateText ~ id.name; const(Thing)* thing = fqn in s; if (thing is null) - currentScope[fqn] = Thing(fqn, name.line, name.column, !fromLabel /+, isConditional+/ ); + { + currentScope[fqn] = Thing(fqn, id.line, id.column, !fromLabel); + } else if (i != 0 || !isConditional) { immutable thisKind = fromLabel ? "Label" : "Variable"; immutable otherKind = thing.isVar ? "variable" : "label"; - addErrorMessage(name, KEY, - thisKind ~ " \"" ~ fqn ~ "\" has the same name as a " - ~ otherKind ~ " defined on line " ~ to!string(thing.line) ~ "."); + auto msg = thisKind ~ " \"" ~ fqn ~ "\" has the same name as a " + ~ otherKind ~ " defined on line " ~ to!string(thing.line) ~ "."; + addErrorMessage(id.line, id.column, KEY, msg); } ++i; } } - static struct Thing + extern (D) static struct Thing { string name; size_t line; size_t column; bool isVar; - //bool isConditional; } - ref currentScope() @property + extern (D) ref currentScope() @property { return stack[$ - 1]; } - protected override void pushScope() + extern (D) void pushScope() { stack.length++; } - protected override void popScope() + extern (D) void popScope() { stack.length--; } - int conditionalDepth; - - void pushAggregateName(Token name) + extern (D) void pushAggregateName(string name, size_t line, size_t column) { - parentAggregates ~= name; + parentAggregates ~= Thing(name, line, column); updateAggregateText(); } - void popAggregateName() + extern (D) void popAggregateName() { parentAggregates.length -= 1; updateAggregateText(); } - void updateAggregateText() + extern (D) void updateAggregateText() { import std.algorithm : map; import std.array : join; if (parentAggregates.length) - parentAggregateText = parentAggregates.map!(a => a.text).join(".") ~ "."; + parentAggregateText = parentAggregates.map!(a => a.name).join(".") ~ "."; else parentAggregateText = ""; } - - Token[] parentAggregates; - string parentAggregateText; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.label_var_same_name_check = Check.enabled; - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ unittest { blah: - int blah; /+ - ^^^^ [warn]: Variable "blah" has the same name as a label defined on line 4. +/ + int blah; // [warn]: Variable "blah" has the same name as a label defined on line 4. } int blah; unittest { static if (stuff) int a; - int a; /+ - ^ [warn]: Variable "a" has the same name as a variable defined on line 12. +/ + int a; // [warn]: Variable "a" has the same name as a variable defined on line 11. } unittest @@ -188,8 +317,7 @@ unittest int a = 10; else int a = 20; - int a; /+ - ^ [warn]: Variable "a" has the same name as a variable defined on line 30. +/ + int a; // [warn]: Variable "a" has the same name as a variable defined on line 28. } template T(stuff) { @@ -212,8 +340,7 @@ unittest int c = 10; else int c = 20; - int c; /+ - ^ [warn]: Variable "c" has the same name as a variable defined on line 54. +/ + int c; // [warn]: Variable "c" has the same name as a variable defined on line 51. } unittest @@ -251,8 +378,7 @@ unittest interface A { int a; - int a; /+ - ^ [warn]: Variable "A.a" has the same name as a variable defined on line 93. +/ + int a; // [warn]: Variable "A.a" has the same name as a variable defined on line 89. } } @@ -276,7 +402,6 @@ unittest break; } } - }c, sac); stderr.writeln("Unittest for LabelVarNameCheck passed."); } diff --git a/src/dscanner/analysis/lambda_return_check.d b/src/dscanner/analysis/lambda_return_check.d index 583154c0..cd5a6a66 100644 --- a/src/dscanner/analysis/lambda_return_check.d +++ b/src/dscanner/analysis/lambda_return_check.d @@ -5,84 +5,117 @@ module dscanner.analysis.lambda_return_check; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.utils : safeAccess; +import dmd.tokens : Token, TOK; -final class LambdaReturnCheck : BaseAnalyzer +extern (C++) class LambdaReturnCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"lambda_return_check"; - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.confusing.lambda_returns_lambda"; + private enum MSG = "This lambda returns a lambda. Add parenthesis to clarify."; + + private Token[] tokens; + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); + lexFile(); } - override void visit(const FunctionLiteralExpression fLit) + private void lexFile() { - import std.algorithm : find; + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + while (lexer.nextToken() != TOK.endOfFile) + tokens ~= lexer.token; + } - auto fe = safeAccess(fLit).assignExpression.as!UnaryExpression - .primaryExpression.functionLiteralExpression.unwrap; + override void visit(AST.FuncLiteralDeclaration lambda) + { + import std.algorithm.iteration : filter; + import std.algorithm.searching : canFind, find, until; - if (fe is null || fe.parameters !is null || fe.identifier != tok!"" || - fe.specifiedFunctionBody is null || fe.specifiedFunctionBody.blockStatement is null) - { + super.visit(lambda); + + if (lambda.fbody.isReturnStatement() is null) return; - } - auto start = &fLit.tokens[0]; - auto endIncl = &fe.specifiedFunctionBody.tokens[0]; - assert(endIncl >= start); - auto tokens = start[0 .. endIncl - start + 1]; - auto arrow = tokens.find!(a => a.type == tok!"=>"); - - AutoFix[] autofixes; - if (arrow.length) + + auto lambdaRange = tokens.filter!(t => t.loc.fileOffset > lambda.loc.fileOffset) + .filter!(t => t.loc.fileOffset < lambda.endloc.fileOffset); + auto tokenRange = lambdaRange.find!(t => t.value == TOK.goesTo); + + if (!tokenRange.canFind!(t => t.value == TOK.leftCurly)) + return; + + if (!tokenRange.canFind!(t => t.value == TOK.leftParenthesis)) { - if (fLit.tokens[0] == tok!"(") - autofixes ~= AutoFix.replacement(arrow[0], "", "Remove arrow (use function body)"); + auto start = tokenRange.front.loc.fileOffset; + AutoFix fix0; + auto firstParam = (*(lambda.getParameterList().parameters))[0]; + + if (hasParensOnParams((*(lambda.getParameterList().parameters))[0])) + fix0 = AutoFix.replacement(start, start + 3, "", "Remove arrow (use function body)"); else - autofixes ~= AutoFix.insertionBefore(fLit.tokens[0], "(", "Remove arrow (use function body)") - .concat(AutoFix.insertionAfter(fLit.tokens[0], ")")) - .concat(AutoFix.replacement(arrow[0], "")); + fix0 = AutoFix.insertionAt(firstParam.loc.fileOffset, "(") + .concat(AutoFix.insertionAt(start - 1, ")")) + .concat(AutoFix.replacement(start, start + 3, "", "Remove arrow (use function body)")); + + addErrorMessage( + cast(ulong) lambda.loc.linnum, cast(ulong) lambda.loc.charnum, KEY, MSG, + [fix0, AutoFix.insertionAt(start + 2, " ()")] + ); } - autofixes ~= AutoFix.insertionBefore(*endIncl, "() ", "Add parenthesis (return delegate)"); - addErrorMessage(tokens, KEY, "This lambda returns a lambda. Add parenthesis to clarify.", - autofixes); } -private: - enum KEY = "dscanner.confusing.lambda_returns_lambda"; + private bool hasParensOnParams(AST.Parameter param) + { + int idx; + + foreach (token; tokens) + { + if (token.loc.fileOffset == param.loc.fileOffset) + break; + idx++; + } + + return tokens[idx - 1].value == TOK.leftParenthesis && tokens[idx - 2].value == TOK.leftParenthesis; + } } -version(Windows) {/*because of newline in code*/} else unittest { import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; import std.stdio : stderr; + import std.format : format; StaticAnalysisConfig sac = disabledConfig(); sac.lambda_return_check = Check.enabled; + auto msg = "This lambda returns a lambda. Add parenthesis to clarify."; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void main() { int[] b; - auto a = b.map!(a => { return a * a + 2; }).array(); /+ - ^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ - pragma(msg, typeof(a => { return a; })); /+ - ^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ - pragma(msg, typeof((a) => { return a; })); /+ - ^^^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/ + auto a = b.map!(a => { return a * a + 2; }).array(); // [warn]: %s + pragma(msg, typeof(a => { return a; })); // [warn]: %s + pragma(msg, typeof((a) => { return a; })); // [warn]: %s pragma(msg, typeof({ return a; })); pragma(msg, typeof(a => () { return a; })); + b.map!(a => a * 2); } - }c, sac); - + }c.format(msg, msg, msg), sac); assertAutoFix(q{ void main() diff --git a/src/dscanner/analysis/length_subtraction.d b/src/dscanner/analysis/length_subtraction.d index 40a586ee..32c92292 100644 --- a/src/dscanner/analysis/length_subtraction.d +++ b/src/dscanner/analysis/length_subtraction.d @@ -5,65 +5,53 @@ module dscanner.analysis.length_subtraction; -import std.stdio; - -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_; /** * Checks for subtraction from a .length property. This is usually a bug. */ -final class LengthSubtractionCheck : BaseAnalyzer +extern (C++) class LengthSubtractionCheck(AST) : BaseAnalyzerDmd { - private enum string KEY = "dscanner.suspicious.length_subtraction"; - - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"length_subtraction_check"; - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.suspicious.length_subtraction"; + private enum MSG = "Avoid subtracting from '.length' as it may be unsigned."; + + extern(D) this(string fileName) { - super(args); + super(fileName); } - override void visit(const AddExpression addExpression) + override void visit(AST.MinExp minExpr) { - if (addExpression.operator == tok!"-") - { - const UnaryExpression l = cast(const UnaryExpression) addExpression.left; - const UnaryExpression r = cast(const UnaryExpression) addExpression.right; - if (l is null || r is null) - goto end; - if (r.primaryExpression is null || r.primaryExpression.primary.type != tok!"intLiteral") - goto end; - if (l.identifierOrTemplateInstance is null - || l.identifierOrTemplateInstance.identifier.text != "length") - goto end; - addErrorMessage(addExpression, KEY, - "Avoid subtracting from '.length' as it may be unsigned.", - [ - AutoFix.insertionBefore(l.tokens[0], "cast(ptrdiff_t) ", "Cast to ptrdiff_t") - ]); - } - end: - addExpression.accept(this); + super.visit(minExpr); + + auto left = minExpr.e1.isDotIdExp(); + if (left is null || left.ident is null) + return; + + if (left.ident.toString() == "length") + addErrorMessage( + cast(ulong) left.loc.linnum, cast(ulong) left.loc.charnum, KEY, MSG, + [AutoFix.insertionAt(minExpr.loc.fileOffset, "cast(ptrdiff_t) ")] + ); } } unittest { - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.length_subtraction_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testSizeT() { - if (i < a.length - 1) /+ - ^^^^^^^^^^^^ [warn]: Avoid subtracting from '.length' as it may be unsigned. +/ + if (i < a.length - 1) // [warn]: Avoid subtracting from '.length' as it may be unsigned. writeln("something"); } }c, sac); @@ -81,5 +69,6 @@ unittest writeln("something"); } }c, sac); + stderr.writeln("Unittest for IfElseSameCheck passed."); } diff --git a/src/dscanner/analysis/line_length.d b/src/dscanner/analysis/line_length.d index 38b4cc7e..5693a4eb 100644 --- a/src/dscanner/analysis/line_length.d +++ b/src/dscanner/analysis/line_length.d @@ -6,176 +6,143 @@ module dscanner.analysis.line_length; import dscanner.analysis.base; - -import dparse.ast; -import dparse.lexer; - -import std.typecons : tuple, Tuple; +import dmd.tokens : Token, TOK; /** * Checks for lines longer than `max_line_length` characters */ -final class LineLengthCheck : BaseAnalyzer +extern (C++) class LineLengthCheck : BaseAnalyzerDmd { mixin AnalyzerInfo!"long_line_check"; + private enum KEY = "dscanner.style.long_line"; + immutable string msg; + + private Token[] tokens; + private immutable int maxLineLength; + private uint currentLine = 1; + private int currentLineLen; - /// - this(BaseAnalyzerArguments args, int maxLineLength) + extern (D) this(string fileName, bool skipTests = false, int maxLineLength = 120) { - super(args); + import std.conv : to; + + super(fileName, skipTests); this.maxLineLength = maxLineLength; - } + msg = "Line is longer than " ~ to!string(maxLineLength) ~ " characters"; - override void visit(const Module) - { - size_t endColumn; - lastErrorLine = ulong.max; - foreach (i, token; tokens) - { - immutable info = tokenLength(token, i > 0 ? tokens[i - 1].line : 0); - if (info.multiLine) - endColumn = checkMultiLineToken(token, endColumn); - else if (info.newLine) - endColumn = info.length + token.column - 1; - else - { - immutable wsChange = i > 0 - ? token.column - (tokens[i - 1].column + tokenByteLength(tokens[i - 1])) - : 0; - endColumn += wsChange + info.length; - } - if (endColumn > maxLineLength) - triggerError(token); - } + lexFile(); + checkFile(); } - alias visit = BaseAnalyzer.visit; + private void lexFile() + { + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; -private: + auto bytes = readFile(fileName) ~ '\0'; - ulong lastErrorLine = ulong.max; + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; - void triggerError(ref const Token tok) - { - import std.algorithm : max; + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, true, + true, true, errorSinkNull, &global.compileEnv); - if (tok.line != lastErrorLine) + while (lexer.token.value != TOK.endOfFile) { - addErrorMessage([0, 0], tok.line, [maxLineLength, max(maxLineLength + 1, tok.column + 1)], KEY, message); - lastErrorLine = tok.line; + lexer.nextToken(); + tokens ~= lexer.token; } } - static bool isLineSeparator(dchar c) - { - import std.uni : lineSep, paraSep; - return c == lineSep || c == '\n' || c == '\v' || c == '\r' || c == paraSep; - } - - size_t checkMultiLineToken()(auto ref const Token tok, size_t startColumn = 0) + private void checkFile() { - import std.utf : byDchar; + import std.conv : to; - auto col = startColumn; - foreach (c; tok.text.byDchar) + foreach (i, token; tokens) { - if (isLineSeparator(c)) + switch (token.value) { - if (col > maxLineLength) - triggerError(tok); - col = 1; + case TOK.whitespace: + switch (token.ptr[0]) + { + case '\t': + currentLineLen += 4; + break; + case '\r': + break; + case '\n', '\v': + checkCurrentLineLength(); + break; + default: + for (auto p = token.ptr; *p == ' '; p++) + currentLineLen++; + } + break; + case TOK.comment: + if (i == tokens.length - 1) + skipComment(to!string(token.ptr)); + else + skipComment(token.ptr[0 .. tokens[i + 1].ptr - token.ptr]); + break; + case TOK.string_: + if (i == tokens.length - 1) + checkStringLiteral(to!string(token.ptr)); + else + checkStringLiteral(token.ptr[0 .. tokens[i + 1].ptr - token.ptr]); + break; + default: + currentLineLen += token.toString().length; } - else - col += getEditorLength(c); } - return col; } - unittest + private extern (D) void skipComment(const(char)[] commentStr) { - assert(new LineLengthCheck(BaseAnalyzerArguments.init, 120).checkMultiLineToken(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 8); - assert(new LineLengthCheck(BaseAnalyzerArguments.init, 120).checkMultiLineToken(Token(tok!"stringLiteral", " \na", 0, 0, 0)) == 2); - assert(new LineLengthCheck(BaseAnalyzerArguments.init, 120).checkMultiLineToken(Token(tok!"stringLiteral", " \n ", 0, 0, 0)) == 5); - } - - static size_t tokenByteLength()(auto ref const Token tok) - { - return tok.text is null ? str(tok.type).length : tok.text.length; - } - - unittest - { - assert(tokenByteLength(Token(tok!"stringLiteral", "aaa", 0, 0, 0)) == 3); - assert(tokenByteLength(Token(tok!"stringLiteral", "Дистан", 0, 0, 0)) == 12); - // tabs and whitespace - assert(tokenByteLength(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 1); - assert(tokenByteLength(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 4); - } + import std.utf : byDchar; - // D Style defines tabs to have a width of four spaces - static size_t getEditorLength(C)(C c) - { - if (c == '\t') - return 4; - else - return 1; + foreach (dchar c; commentStr.byDchar) + if (c == '\n' || c == '\v') + checkCurrentLineLength(); } - alias TokenLength = Tuple!(size_t, "length", bool, "newLine", bool, "multiLine"); - static TokenLength tokenLength()(auto ref const Token tok, size_t prevLine) + private extern (D) void checkStringLiteral(const(char)[] str) { import std.utf : byDchar; - size_t length; - const newLine = tok.line > prevLine; - bool multiLine; - if (tok.text is null) - length += str(tok.type).length; - else - foreach (c; tok.text.byDchar) - { - if (isLineSeparator(c)) - { - length = 1; - multiLine = true; - } - else - length += getEditorLength(c); - } - - return TokenLength(length, newLine, multiLine); + foreach (dchar c; str.byDchar) + { + if (c == '\t') + currentLineLen += 4; + else if (c == '\n' || c == '\v') + checkCurrentLineLength(); + else if (c != '\r') + currentLineLen++; + } } - unittest + void checkCurrentLineLength() { - assert(tokenLength(Token(tok!"stringLiteral", "aaa", 0, 0, 0), 0).length == 3); - assert(tokenLength(Token(tok!"stringLiteral", "Дистан", 0, 0, 0), 0).length == 6); - // tabs and whitespace - assert(tokenLength(Token(tok!"stringLiteral", " ", 0, 0, 0), 0).length == 4); - assert(tokenLength(Token(tok!"stringLiteral", " ", 0, 0, 0), 0).length == 4); - } - - import std.conv : to; + if (currentLineLen > maxLineLength) + addErrorMessage(cast(ulong) currentLine, 0uL, KEY, msg); - string message() const - { - return "Line is longer than " ~ to!string(maxLineLength) ~ " characters"; + currentLine++; + currentLineLen = 0; } - - enum string KEY = "dscanner.style.long_line"; - const int maxLineLength; } @system unittest { import dscanner.analysis.config : Check, StaticAnalysisConfig, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.long_line_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ Window window = Platform.instance.createWindow("Дистанционное управление сварочным оборудованием ", null); Window window = Platform.instance.createWindow("Дистанционное управление сварочным оборудованием ", null); // [warn]: Line is longer than 120 characters unittest { @@ -188,12 +155,13 @@ assert("foo" == "foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } }c, sac); -// TODO: libdparse counts columns bytewise - //assert("foo" == "boooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo5"); - //assert("foo" == "booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo6"); // [warn]: Line is longer than 120 characters + assertAnalyzerWarningsDMD(q{ + static assert("foo" == "booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo5"); + static assert("foo" == "boooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo6"); // [warn]: Line is longer than 120 characters + }c, sac); // reduced from std/regex/internal/thompson.d - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ // whitespace on purpose, do not remove! mixin(`case IR.`~e~`: opCacheTrue[pc] = &Ops!(true).op!(IR.`~e~`); @@ -208,7 +176,7 @@ assert("foo" == "foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo // Test customizing max_line_length. sac.max_line_length = 115; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ Window window = Platform.instance.createWindow("Дистанционное управлсварочным оборудованием ", null); Window window = Platform.instance.createWindow("Дистанционное управлсварочным оборудованием ", null); // [warn]: Line is longer than 115 characters unittest { diff --git a/src/dscanner/analysis/local_imports.d b/src/dscanner/analysis/local_imports.d index f6df2b24..424d69cd 100644 --- a/src/dscanner/analysis/local_imports.d +++ b/src/dscanner/analysis/local_imports.d @@ -5,101 +5,99 @@ module dscanner.analysis.local_imports; -import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_; /** * Checks for local imports that import all symbols. * See_also: $(LINK https://issues.dlang.org/show_bug.cgi?id=10378) */ -final class LocalImportCheck : BaseAnalyzer +extern(C++) class LocalImportCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - mixin AnalyzerInfo!"local_import_check"; + alias visit = BaseAnalyzerDmd.visit; - /** - * Construct with the given file name. - */ - this(BaseAnalyzerArguments args) + mixin ScopedVisit!(AST.FuncDeclaration); + mixin ScopedVisit!(AST.IfStatement); + mixin ScopedVisit!(AST.WhileStatement); + mixin ScopedVisit!(AST.ForStatement); + mixin ScopedVisit!(AST.ForeachStatement); + mixin ScopedVisit!(AST.ClassDeclaration); + mixin ScopedVisit!(AST.StructDeclaration); + + extern(D) this(string fileName) { - super(args); + super(fileName); + this.localImport = false; } - mixin visitThing!StructBody; - mixin visitThing!BlockStatement; - - override void visit(const Declaration dec) + override void visit(AST.Import i) { - if (dec.importDeclaration is null) - { - dec.accept(this); - return; - } - foreach (attr; dec.attributes) + // Look for import foo.bar : x or foo.bar : y = x + if (!i.isstatic && localImport && i.names.length == 0 && !i.aliasId) { - if (attr.attribute == tok!"static") - isStatic = true; + addErrorMessage(cast(ulong) i.loc.linnum, cast(ulong) i.loc.charnum, KEY, MESSAGE); } - dec.accept(this); - isStatic = false; } - override void visit(const ImportDeclaration id) + // Skip unittests for now + override void visit(AST.UnitTestDeclaration ud) { - if ((!isStatic && interesting) && (id.importBindings is null - || id.importBindings.importBinds.length == 0)) - { - foreach (singleImport; id.singleImports) - { - if (singleImport.rename.text.length == 0) - { - addErrorMessage(singleImport, - KEY, "Local imports should specify" - ~ " the symbols being imported to avoid hiding local symbols."); - } - } - } + return; } private: - - enum string KEY = "dscanner.suspicious.local_imports"; - - mixin template visitThing(T) + template ScopedVisit(NodeType) { - override void visit(const T thing) + override void visit(NodeType n) { - const b = interesting; - interesting = true; - thing.accept(this); - interesting = b; + bool prevState = localImport; + localImport = true; + super.visit(n); + localImport = prevState; } } - bool interesting; - bool isStatic; + bool localImport; + enum KEY = "dscanner.suspicious.local_imports"; + enum MESSAGE = "Local imports should specify the symbols being imported to avoid hiding local symbols."; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.local_import_check = Check.enabled; - assertAnalyzerWarnings(q{ - void testLocalImport() + + assertAnalyzerWarningsDMD(q{ + import std.experimental; + + void foo() { - import std.stdio; /+ - ^^^^^^^^^ [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. +/ + import std.stdio; // [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. import std.fish : scales, head; import DAGRON = std.experimental.dragon; + + if (1) + { + import foo.bar; // [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. + } + else + { + import foo.bar; // [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. + } + + foreach (i; [1, 2, 3]) + { + import foo.bar; // [warn]: Local imports should specify the symbols being imported to avoid hiding local symbols. + import std.stdio : writeln; + } } + + import std.experimental.dragon; }c, sac); stderr.writeln("Unittest for LocalImportCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/logic_precedence.d b/src/dscanner/analysis/logic_precedence.d index d08ee552..ec0871c1 100644 --- a/src/dscanner/analysis/logic_precedence.d +++ b/src/dscanner/analysis/logic_precedence.d @@ -5,12 +5,8 @@ module dscanner.analysis.logic_precedence; -import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_; /** * Checks for code with confusing && and || operator precedence @@ -19,48 +15,62 @@ import dsymbol.scope_; * if (a && (b || c)) // good * --- */ -final class LogicPrecedenceCheck : BaseAnalyzer +extern (C++) class LogicPrecedenceCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - enum string KEY = "dscanner.confusing.logical_precedence"; mixin AnalyzerInfo!"logical_precedence_check"; + alias visit = BaseAnalyzerDmd.visit; - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const OrOrExpression orOr) + override void visit(AST.LogicalExp le) { - if (orOr.left is null || orOr.right is null) - return; - const AndAndExpression left = cast(AndAndExpression) orOr.left; - const AndAndExpression right = cast(AndAndExpression) orOr.right; - if (left is null && right is null) - return; - if ((left !is null && left.right is null) && (right !is null && right.right is null)) - return; - addErrorMessage(orOr, KEY, - "Use parenthesis to clarify this expression."); - orOr.accept(this); + import dmd.tokens : EXP; + + auto left = le.e1.isLogicalExp(); + auto right = le.e2.isLogicalExp(); + + if (left) + left = left.op == EXP.andAnd ? left : null; + if (right) + right = right.op == EXP.andAnd ? right : null; + + if (le.op != EXP.orOr) + goto END; + + if (!left && !right) + goto END; + + if ((left && left.parens) || (right && right.parens)) + goto END; + + if ((left !is null && left.e2 is null) && (right !is null && right.e2 is null)) + goto END; + + addErrorMessage(cast(ulong) le.loc.linnum, cast(ulong) le.loc.charnum, + KEY, "Use parenthesis to clarify this expression."); + + END: + super.visit(le); } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.logical_precedence_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testFish() { - if (a && b || c) {} /+ - ^^^^^^^^^^^ [warn]: Use parenthesis to clarify this expression. +/ + if (a && b || c) {} // [warn]: Use parenthesis to clarify this expression. if ((a && b) || c) {} // Good - if (b || c && d) {} /+ - ^^^^^^^^^^^ [warn]: Use parenthesis to clarify this expression. +/ + if (b || c && d) {} // [warn]: Use parenthesis to clarify this expression. if (b || (c && d)) {} // Good } }c, sac); diff --git a/src/dscanner/analysis/mismatched_args.d b/src/dscanner/analysis/mismatched_args.d index db75eb4c..3c044157 100644 --- a/src/dscanner/analysis/mismatched_args.d +++ b/src/dscanner/analysis/mismatched_args.d @@ -1,263 +1,186 @@ module dscanner.analysis.mismatched_args; import dscanner.analysis.base; -import dscanner.utils : safeAccess; -import dsymbol.scope_; -import dsymbol.symbol; -import dparse.ast; -import dparse.lexer : tok, Token; -import dsymbol.builtin.names; +import std.format : format; +import dmd.arraytypes : Identifiers; /// Checks for mismatched argument and parameter names -final class MismatchedArgumentCheck : BaseAnalyzer +extern (C++) class MismatchedArgumentCheck(AST) : BaseAnalyzerDmd { + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"mismatched_args_check"; - /// - this(BaseAnalyzerArguments args) + private enum string KEY = "dscanner.confusing.argument_parameter_mismatch"; + private enum string MSG = "Argument %d is named '%s', but this is the name of parameter %d"; + + private string[][string] funcsWithParams; + private bool inFunction; + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const FunctionCallExpression fce) + override void visit(AST.Module moduleNode) { - import std.typecons : scoped; - import std.algorithm.iteration : each, map; - import std.array : array; + import dmd.astcodegen : ASTCodegen; - if (fce.arguments is null) - return; - auto argVisitor = scoped!ArgVisitor; - argVisitor.visit(fce.arguments); - const istring[] args = argVisitor.args; + auto argVisitor = new ParameterVisitor!(ASTCodegen)(fileName, skipTests); + argVisitor.visit(moduleNode); + funcsWithParams = argVisitor.funcsWithParams; + + super.visit(moduleNode); + } - auto identVisitor = scoped!IdentVisitor; - if (fce.unaryExpression !is null) - identVisitor.visit(fce.unaryExpression); - else if (fce.type !is null) - identVisitor.visit(fce.type); + override void visit(AST.CallExp callExpr) + { + super.visit(callExpr); - const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names.length > 0 - ? identVisitor.names : [CONSTRUCTOR_SYMBOL_NAME]); + auto funcIdent = callExpr.e1.isIdentifierExp(); + if (callExpr.arguments is null || funcIdent is null || funcIdent.ident is null) + return; - static struct ErrorMessage - { - const(Token)[] range; - string message; - } + string funcName = cast(string) funcIdent.ident.toString(); + if ((funcName in funcsWithParams) is null || (*callExpr.arguments).length != funcsWithParams[funcName].length) + return; - ErrorMessage[] messages; - bool matched; + auto namedArgsPositions = getNamedArgsPositions(callExpr.names, funcName); + string[] args; - foreach (sym; symbols) + foreach (int idx, arg; *callExpr.arguments) { - // The cast is a hack because .array() confuses the compiler's overload - // resolution code. - const(istring)[] params = sym is null ? [] : sym.argNames[].map!(a => cast() a).array(); - const ArgMismatch[] mismatches = compareArgsToParams(params, args); - if (mismatches.length == 0) - matched = true; - else + if (auto identifier = arg.isIdentifierExp()) { - foreach (ref const mm; mismatches) + if (identifier.ident) + { + if ((idx in namedArgsPositions) is null) + args ~= cast(string) identifier.ident.toString(); + else + args ~= ""; + } + else { - messages ~= ErrorMessage(argVisitor.tokens[mm.argIndex], createWarningFromMismatch(mm)); + return; } } + else + { + args ~= ""; + } } - if (!matched) - foreach (m; messages) - addErrorMessage(m.range, KEY, m.message); - } - - alias visit = ASTVisitor.visit; - -private: - - enum string KEY = "dscanner.confusing.argument_parameter_mismatch"; -} - -final class IdentVisitor : ASTVisitor -{ - override void visit(const IdentifierOrTemplateInstance ioti) - { - import dsymbol.string_interning : internString; + foreach_reverse (argIdx, arg; args) + { + foreach_reverse (paramIdx, param; funcsWithParams[funcName]) + { + if (arg == param) + { + if (argIdx == paramIdx) + break; - if (ioti.identifier != tok!"") - names ~= internString(ioti.identifier.text); - else - names ~= internString(ioti.templateInstance.identifier.text); - } + addErrorMessage(callExpr.loc.linnum, callExpr.loc.charnum,KEY, + MSG.format(argIdx + 1, arg, paramIdx + 1)); - override void visit(const Arguments) - { + return; + } + } + } } - override void visit(const IndexExpression ie) + private extern (D) bool[int] getNamedArgsPositions(Identifiers* names, string funcName) { - if (ie.unaryExpression !is null) - visit(ie.unaryExpression); - } + bool[int] namedArgsPositions; - alias visit = ASTVisitor.visit; + if (names is null || (funcName in funcsWithParams) is null) + return namedArgsPositions; - istring[] names; -} + auto funcParams = funcsWithParams[funcName]; -final class ArgVisitor : ASTVisitor -{ - override void visit(const NamedArgumentList al) - { - foreach (a; al.items) + foreach (name; *names) { - auto u = cast(UnaryExpression) a.assignExpression; - size_t prevArgs = args.length; - if (u !is null && !a.name.text.length) - visit(u); - - if (args.length == prevArgs) - { - // if we didn't get an identifier in the unary expression, - // assume it's a good argument - args ~= istring.init; - tokens ~= a.tokens; - } - } - } + if (name is null) + continue; - override void visit(const UnaryExpression unary) - { - import dsymbol.string_interning : internString; + string argName = cast(string) name.toString(); + int idx; + for (idx = 0; idx < funcParams.length; idx++) + if (funcParams[idx] == argName) + break; - if (auto iot = unary.safeAccess.primaryExpression.identifierOrTemplateInstance.unwrap) - { - if (iot.identifier == tok!"") - return; - immutable t = iot.identifier; - tokens ~= [t]; - args ~= internString(t.text); + namedArgsPositions[idx] = true; } - } - - alias visit = ASTVisitor.visit; - const(Token[])[] tokens; - istring[] args; + return namedArgsPositions; + } } -const(DSymbol)*[] resolveSymbol(const Scope* sc, const istring[] symbolChain) +extern (C++) class ParameterVisitor(AST) : BaseAnalyzerDmd { - import std.array : empty; + alias visit = BaseAnalyzerDmd.visit; - const(DSymbol)*[] matchingSymbols = sc.getSymbolsByName(symbolChain[0]); - if (matchingSymbols.empty) - return null; + public string[][string] funcsWithParams; + private string currentFunc; + private bool ignoreCurrentFunc; + private bool inFunction; - foreach (ref symbol; matchingSymbols) + extern (D) this(string fileName, bool skipTests = false) { - inner: foreach (i; 1 .. symbolChain.length) - { - if (symbol.kind == CompletionKind.variableName - || symbol.kind == CompletionKind.memberVariableName - || symbol.kind == CompletionKind.functionName - || symbol.kind == CompletionKind.aliasName) - symbol = symbol.type; - if (symbol is null) - { - symbol = null; - break inner; - } - auto p = symbol.getPartsByName(symbolChain[i]); - if (p.empty) - { - symbol = null; - break inner; - } - symbol = p[0]; - } + super(fileName, skipTests); } - return matchingSymbols; -} -struct ArgMismatch -{ - size_t argIndex; - size_t paramIndex; - string name; -} + override void visit(AST.TemplateDeclaration templateDecl) + { + if (inFunction) + return; -immutable(ArgMismatch[]) compareArgsToParams(const istring[] params, const istring[] args) pure -{ - import std.exception : assumeUnique; + inFunction = true; + super.visit(templateDecl); + } - if (args.length != params.length) - return []; - ArgMismatch[] retVal; - foreach (i, arg; args) + override void visit(AST.FuncDeclaration funcDecl) { - if (arg is null || arg == params[i]) - continue; - foreach (j, param; params) - if (param == arg) - retVal ~= ArgMismatch(i, j, arg); - } - return assumeUnique(retVal); -} + if (inFunction) + return; -string createWarningFromMismatch(const ArgMismatch mismatch) pure -{ - import std.format : format; + inFunction = true; + string lastFunc = currentFunc; + currentFunc = cast(string) funcDecl.ident.toString(); + funcsWithParams[currentFunc] = []; - return "Argument %d is named '%s', but this is the name of parameter %d".format( - mismatch.argIndex + 1, mismatch.name, mismatch.paramIndex + 1); -} + bool ignoreLast = ignoreCurrentFunc; + ignoreCurrentFunc = false; -unittest -{ - import dsymbol.string_interning : internString; - import std.algorithm.iteration : map; - import std.array : array; - import std.conv : to; + super.visit(funcDecl); - { - istring[] args = ["a", "b", "c"].map!internString().array(); - istring[] params = ["a", "b", "c"].map!internString().array(); - immutable res = compareArgsToParams(params, args); - assert(res == []); - } + if (ignoreCurrentFunc) + funcsWithParams.remove(currentFunc); - { - istring[] args = ["a", "c", "b"].map!internString().array(); - istring[] params = ["a", "b", "c"].map!internString().array(); - immutable res = compareArgsToParams(params, args); - assert(res == [ArgMismatch(1, 2, "c"), ArgMismatch(2, 1, "b")], to!string(res)); + ignoreCurrentFunc = ignoreLast; + currentFunc = lastFunc; } + override void visit(AST.Parameter parameter) { - istring[] args = ["a", "c", "b"].map!internString().array(); - istring[] params = ["alpha", "bravo", "c"].map!internString().array(); - immutable res = compareArgsToParams(params, args); - assert(res == [ArgMismatch(1, 2, "c")]); - } + if (parameter.ident is null) + { + ignoreCurrentFunc = true; + return; + } - { - istring[] args = ["a", "b"].map!internString().array(); - istring[] params = [null, "b"].map!internString().array(); - immutable res = compareArgsToParams(params, args); - assert(res == []); + funcsWithParams[currentFunc] ~= cast(string) parameter.ident.toString(); } } unittest { - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.mismatched_args_check = Check.enabled; - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ void foo(int x, int y) { } @@ -266,20 +189,17 @@ unittest { int x = 1; int y = 2; - foo(y, x); /+ - ^ [warn]: Argument 2 is named 'x', but this is the name of parameter 1 +/ - foo(y + 1, x); /+ - ^ [warn]: Argument 2 is named 'x', but this is the name of parameter 1 +/ + foo(y, x); // [warn]: Argument 2 is named 'x', but this is the name of parameter 1 + foo(y + 1, x); // [warn]: Argument 2 is named 'x', but this is the name of parameter 1 foo(y + 1, f(x)); foo(x: y, y: x); - foo(y, 0); /+ - ^ [warn]: Argument 1 is named 'y', but this is the name of parameter 2 +/ + foo(y, 0); // [warn]: Argument 1 is named 'y', but this is the name of parameter 2 // foo(y: y, x); // TODO: this shouldn't error foo(x, y: x); // TODO: this should error - foo(y, y: x); /+ - ^ [warn]: Argument 1 is named 'y', but this is the name of parameter 2 +/ + foo(y, y: x); // [warn]: Argument 1 is named 'y', but this is the name of parameter 2 } }c, sac); + stderr.writeln("Unittest for MismatchedArgumentCheck passed."); } diff --git a/src/dscanner/analysis/nolint.d b/src/dscanner/analysis/nolint.d deleted file mode 100644 index 4d2ab411..00000000 --- a/src/dscanner/analysis/nolint.d +++ /dev/null @@ -1,271 +0,0 @@ -module dscanner.analysis.nolint; - -@safe: - -import dparse.ast; -import dparse.lexer; - -import std.algorithm : canFind; -import std.regex : matchAll, regex; -import std.string : lastIndexOf, strip; -import std.typecons; - -struct NoLint -{ - bool containsCheck(scope const(char)[] check) const - { - while (true) - { - if (disabledChecks.get((() @trusted => cast(string) check)(), 0) > 0) - return true; - - auto dot = check.lastIndexOf('.'); - if (dot == -1) - break; - check = check[0 .. dot]; - } - return false; - } - - // automatic pop when returned value goes out of scope - Poppable push(in Nullable!NoLint other) scope - { - if (other.isNull) - return Poppable(null); - - foreach (key, value; other.get.getDisabledChecks) - this.disabledChecks[key] += value; - - return Poppable(() => this.pop(other)); - } - -package: - const(int[string]) getDisabledChecks() const - { - return this.disabledChecks; - } - - void pushCheck(in string check) - { - disabledChecks[check]++; - } - - void merge(in Nullable!NoLint other) - { - if (other.isNull) - return; - - foreach (key, value; other.get.getDisabledChecks) - this.disabledChecks[key] += value; - } - -private: - void pop(in Nullable!NoLint other) - { - if (other.isNull) - return; - - foreach (key, value; other.get.getDisabledChecks) - { - assert(this.disabledChecks.get(key, 0) >= value); - - this.disabledChecks[key] -= value; - } - } - - static struct Poppable - { - ~this() - { - if (onPop) - onPop(); - onPop = null; - } - - private: - void delegate() onPop; - } - - int[string] disabledChecks; -} - -struct NoLintFactory -{ - static Nullable!NoLint fromModuleDeclaration(in ModuleDeclaration moduleDeclaration) - { - NoLint noLint; - - foreach (atAttribute; moduleDeclaration.atAttributes) - noLint.merge(NoLintFactory.fromAtAttribute(atAttribute)); - - if (!noLint.getDisabledChecks.length) - return nullNoLint; - - return noLint.nullable; - } - - static Nullable!NoLint fromDeclaration(in Declaration declaration) - { - NoLint noLint; - foreach (attribute; declaration.attributes) - noLint.merge(NoLintFactory.fromAttribute(attribute)); - - if (!noLint.getDisabledChecks.length) - return nullNoLint; - - return noLint.nullable; - } - -private: - static Nullable!NoLint fromAttribute(const(Attribute) attribute) - { - if (attribute is null) - return nullNoLint; - - return NoLintFactory.fromAtAttribute(attribute.atAttribute); - - } - - static Nullable!NoLint fromAtAttribute(const(AtAttribute) atAttribute) - { - if (atAttribute is null) - return nullNoLint; - - auto ident = atAttribute.identifier; - auto argumentList = atAttribute.argumentList; - - if (argumentList !is null) - { - if (ident.text.length) - return NoLintFactory.fromStructUda(ident, argumentList); - else - return NoLintFactory.fromStringUda(argumentList); - - } - else - return nullNoLint; - } - - // @nolint("..") - static Nullable!NoLint fromStructUda(in Token ident, in ArgumentList argumentList) - in (ident.text.length && argumentList !is null) - { - if (ident.text != "nolint") - return nullNoLint; - - NoLint noLint; - - foreach (nodeExpr; argumentList.items) - { - if (auto unaryExpr = cast(const UnaryExpression) nodeExpr) - { - auto primaryExpression = unaryExpr.primaryExpression; - if (primaryExpression is null) - continue; - - if (primaryExpression.primary != tok!"stringLiteral") - continue; - - noLint.pushCheck(primaryExpression.primary.text.strip("\"")); - } - } - - if (!noLint.getDisabledChecks().length) - return nullNoLint; - - return noLint.nullable; - } - - // @("nolint(..)") - static Nullable!NoLint fromStringUda(in ArgumentList argumentList) - in (argumentList !is null) - { - NoLint noLint; - - foreach (nodeExpr; argumentList.items) - { - if (auto unaryExpr = cast(const UnaryExpression) nodeExpr) - { - auto primaryExpression = unaryExpr.primaryExpression; - if (primaryExpression is null) - continue; - - if (primaryExpression.primary != tok!"stringLiteral") - continue; - - auto str = primaryExpression.primary.text.strip("\""); - Nullable!NoLint currNoLint = NoLintFactory.fromString(str); - noLint.merge(currNoLint); - } - } - - if (!noLint.getDisabledChecks().length) - return nullNoLint; - - return noLint.nullable; - - } - - // Transform a string with form "nolint(abc, efg)" - // into a NoLint struct - static Nullable!NoLint fromString(in string str) - { - static immutable re = regex(`[\w-_.]+`, "g"); - auto matches = matchAll(str, re); - - if (!matches) - return nullNoLint; - - const udaName = matches.hit; - if (udaName != "nolint") - return nullNoLint; - - matches.popFront; - - NoLint noLint; - - while (matches) - { - noLint.pushCheck(matches.hit); - matches.popFront; - } - - if (!noLint.getDisabledChecks.length) - return nullNoLint; - - return noLint.nullable; - } - - static nullNoLint = Nullable!NoLint.init; -} - -unittest -{ - const s1 = "nolint(abc)"; - const s2 = "nolint(abc, efg, hij)"; - const s3 = " nolint ( abc , efg ) "; - const s4 = "nolint(dscanner.style.abc_efg-ijh)"; - const s5 = "OtherUda(abc)"; - const s6 = "nolint(dscanner)"; - - assert(NoLintFactory.fromString(s1).get.containsCheck("abc")); - - assert(NoLintFactory.fromString(s2).get.containsCheck("abc")); - assert(NoLintFactory.fromString(s2).get.containsCheck("efg")); - assert(NoLintFactory.fromString(s2).get.containsCheck("hij")); - - assert(NoLintFactory.fromString(s3).get.containsCheck("abc")); - assert(NoLintFactory.fromString(s3).get.containsCheck("efg")); - - assert(NoLintFactory.fromString(s4).get.containsCheck("dscanner.style.abc_efg-ijh")); - - assert(NoLintFactory.fromString(s5).isNull); - - assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner")); - assert(!NoLintFactory.fromString(s6).get.containsCheck("dscanner2")); - assert(NoLintFactory.fromString(s6).get.containsCheck("dscanner.foo")); - - import std.stdio : stderr, writeln; - - (() @trusted => stderr.writeln("Unittest for NoLint passed."))(); -} diff --git a/src/dscanner/analysis/numbers.d b/src/dscanner/analysis/numbers.d index a9629966..7599e7e8 100644 --- a/src/dscanner/analysis/numbers.d +++ b/src/dscanner/analysis/numbers.d @@ -5,60 +5,83 @@ module dscanner.analysis.numbers; -import std.stdio; -import std.regex; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; +import dmd.tokens : TOK; +import std.conv; +import std.regex; /** * Checks for long and hard-to-read number literals */ -final class NumberStyleCheck : BaseAnalyzer +extern (C++) class NumberStyleCheck(AST) : BaseAnalyzerDmd { -public: - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"number_style_check"; - /** - * Constructs the style checker with the given file name. - */ - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.style.number_literals"; + private enum string MSG = "Use underscores to improve number constant readability."; + + private auto badBinaryRegex = ctRegex!(`^0b[01]{9,}`); + private auto badDecimalRegex = ctRegex!(`^\d{5,}`); + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const Token t) + override void visit(AST.IntegerExp intExpr) { - import std.algorithm : startsWith; + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + bytes = bytes[intExpr.loc.fileOffset .. $]; + + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; - if (isNumberLiteral(t.type) && !t.text.startsWith("0x") - && ((t.text.startsWith("0b") && !t.text.matchFirst(badBinaryRegex) - .empty) || !t.text.matchFirst(badDecimalRegex).empty)) + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, errorSinkNull, &global.compileEnv); + auto tokenValue = lexer.nextToken(); + bool isInt = false; + + while (tokenValue != TOK.semicolon && tokenValue != TOK.endOfFile) { - addErrorMessage(t, KEY, - "Use underscores to improve number constant readability."); + if (isIntegerLiteral(tokenValue)) + { + isInt = true; + break; + } + + tokenValue = lexer.nextToken(); } - } -private: + if (!isInt) + return; - enum string KEY = "dscanner.style.number_literals"; + auto tokenText = to!string(lexer.token.ptr); - auto badBinaryRegex = ctRegex!(`^0b[01]{9,}`); - auto badDecimalRegex = ctRegex!(`^\d{5,}`); + if (!matchFirst(tokenText, badDecimalRegex).empty || !matchFirst(tokenText, badBinaryRegex).empty) + addErrorMessage(intExpr.loc.linnum, intExpr.loc.charnum, KEY, MSG); + } + + private bool isIntegerLiteral(TOK token) + { + return token >= TOK.int32Literal && token <= TOK.uns128Literal; + } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.number_style_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testNumbers() { int a; @@ -66,12 +89,12 @@ unittest a = 10; // ok a = 100; // ok a = 1000; // ok - a = 10000; /+ - ^^^^^ [warn]: Use underscores to improve number constant readability. +/ - a = 100000; /+ - ^^^^^^ [warn]: Use underscores to improve number constant readability. +/ - a = 1000000; /+ - ^^^^^^^ [warn]: Use underscores to improve number constant readability. +/ + a = 10_00; // ok + a = 10_000; // ok + a = 100_000; // ok + a = 10000; // [warn]: Use underscores to improve number constant readability. + a = 100000; // [warn]: Use underscores to improve number constant readability. + a = 1000000; // [warn]: Use underscores to improve number constant readability. } }c, sac); diff --git a/src/dscanner/analysis/objectconst.d b/src/dscanner/analysis/objectconst.d index e000fe52..cc865e6d 100644 --- a/src/dscanner/analysis/objectconst.d +++ b/src/dscanner/analysis/objectconst.d @@ -5,102 +5,107 @@ module dscanner.analysis.objectconst; -import std.stdio; -import std.regex; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; +import std.stdio; -/** - * Checks that opEquals, opCmp, toHash, 'opCast', and toString are either const, - * immutable, or inout. - */ -final class ObjectConstCheck : BaseAnalyzer +extern(C++) class ObjectConstCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - mixin AnalyzerInfo!"object_const_check"; + alias visit = BaseAnalyzerDmd.visit; - /// - this(BaseAnalyzerArguments args) - { - super(args); - } - - mixin visitTemplate!ClassDeclaration; - mixin visitTemplate!InterfaceDeclaration; - mixin visitTemplate!UnionDeclaration; - mixin visitTemplate!StructDeclaration; - - override void visit(const AttributeDeclaration d) + extern(D) this(string fileName) { - if (d.attribute.attribute == tok!"const" && inAggregate) - { - constColon = true; - } - d.accept(this); + super(fileName); } - override void visit(const Declaration d) + void visitAggregate(AST.AggregateDeclaration ad) { - import std.algorithm : any; - bool setConstBlock; - if (inAggregate && d.attributes && d.attributes.any!(a => a.attribute == tok!"const")) - { - setConstBlock = true; - constBlock = true; - } + import dmd.astenums : MODFlags, STC; - bool containsDisable(A)(const A[] attribs) - { - import std.algorithm.searching : canFind; - return attribs.canFind!(a => a.atAttribute !is null && - a.atAttribute.identifier.text == "disable"); - } + if (!ad.members) + return; - if (const FunctionDeclaration fd = d.functionDeclaration) + foreach(member; *ad.members) { - const isDeclationDisabled = containsDisable(d.attributes) || - containsDisable(fd.memberFunctionAttributes); - - if (inAggregate && !constColon && !constBlock && !isDeclationDisabled - && isInteresting(fd.name.text) && !hasConst(fd.memberFunctionAttributes)) + if (auto fd = member.isFuncDeclaration()) + { + if (isInteresting(fd.ident.toString()) && !isConstFunc(fd) && + !(fd.storage_class & STC.disable)) + addErrorMessage(cast(ulong) fd.loc.linnum, cast(ulong) fd.loc.charnum, KEY, + "Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const."); + + member.accept(this); + } + else if (auto scd = member.isStorageClassDeclaration()) { - addErrorMessage(d.functionDeclaration.name, KEY, - "Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const."); + foreach (smember; *scd.decl) + { + if (auto fd2 = smember.isFuncDeclaration()) + { + if (isInteresting(fd2.ident.toString()) && !isConstFunc(fd2, scd) && + !(fd2.storage_class & STC.disable)) + addErrorMessage(cast(ulong) fd2.loc.linnum, cast(ulong) fd2.loc.charnum, KEY, + "Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const."); + + smember.accept(this); + } + else + smember.accept(this); + } } + else + member.accept(this); } - - d.accept(this); - - if (!inAggregate) - constColon = false; - if (setConstBlock) - constBlock = false; } -private: + override void visit(AST.ClassDeclaration cd) + { + visitAggregate(cd); + } - enum string KEY = "dscanner.suspicious.object_const"; + override void visit(AST.StructDeclaration sd) + { + visitAggregate(sd); + } - static bool hasConst(const MemberFunctionAttribute[] attributes) + override void visit(AST.InterfaceDeclaration id) { - import std.algorithm : any; + visitAggregate(id); + } - return attributes.any!(a => a.tokenType == tok!"const" - || a.tokenType == tok!"immutable" || a.tokenType == tok!"inout"); + override void visit(AST.UnionDeclaration ud) + { + visitAggregate(ud); } - static bool isInteresting(string name) + extern(D) private static bool isInteresting(const char[] name) { return name == "opCmp" || name == "toHash" || name == "opEquals" || name == "toString" || name == "opCast"; } - bool constBlock; - bool constColon; + /** + * Checks if a function has either one of attributes `const`, `immutable`, `inout` + */ + private bool isConstFunc(AST.FuncDeclaration fd, AST.StorageClassDeclaration scd = null) + { + import dmd.astenums : MODFlags, STC; + import std.stdio : writeln; + + if (scd && (scd.stc & STC.const_ || scd.stc & STC.immutable_ || scd.stc & STC.wild)) + return true; + + if(fd.type && (fd.type.mod == MODFlags.const_ || + fd.type.mod == MODFlags.immutable_ || fd.type.mod == MODFlags.wild)) + return true; + + return false; + } + + private enum KEY = "dscanner.suspicious.object_const"; + + AST.AggregateDeclaration deleteme; } unittest @@ -109,7 +114,7 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.object_const_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testConsts() { // Will be ok because all are declared const/immutable @@ -125,7 +130,7 @@ unittest return 1; } - const hash_t toHash() // ok + immutable hash_t toHash() // ok { return 0; } @@ -143,7 +148,7 @@ unittest class Fox { - const{ override string toString() { return "foo"; }} // ok + inout { override string toString() { return "foo"; } } // ok } class Rat @@ -159,26 +164,22 @@ unittest // Will warn, because none are const class Dog { - bool opEquals(Object a, Object b) /+ - ^^^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ + bool opEquals(Object a, Object b) // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. { return true; } - int opCmp(Object o) /+ - ^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ + int opCmp(Object o) // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. { return 1; } - hash_t toHash() /+ - ^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ + hash_t toHash() // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. { return 0; } - string toString() /+ - ^^^^^^^^ [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. +/ + string toString() // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const. { return "Dog"; } diff --git a/src/dscanner/analysis/opequals_without_tohash.d b/src/dscanner/analysis/opequals_without_tohash.d index 8e7de648..292bf730 100644 --- a/src/dscanner/analysis/opequals_without_tohash.d +++ b/src/dscanner/analysis/opequals_without_tohash.d @@ -5,118 +5,101 @@ module dscanner.analysis.opequals_without_tohash; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; -import std.stdio; -import std.typecons : Rebindable; /** * Checks for when a class/struct has the method opEquals without toHash, or * toHash without opEquals. */ -final class OpEqualsWithoutToHashCheck : BaseAnalyzer +extern(C++) class OpEqualsWithoutToHashCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - mixin AnalyzerInfo!"opequals_tohash_check"; + alias visit = BaseAnalyzerDmd.visit; - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const ClassDeclaration node) + override void visit(AST.ClassDeclaration cd) { - actualCheck(node.name, node.structBody); - node.accept(this); + visitBaseClasses(cd); + visitAggregate(cd); } - override void visit(const StructDeclaration node) + override void visit(AST.StructDeclaration sd) { - actualCheck(node.name, node.structBody); - node.accept(this); + visitAggregate(sd); } - private void actualCheck(const Token name, const StructBody structBody) + private void isInteresting(AST.FuncDeclaration fd, ref bool hasOpEquals, ref bool hasToHash) { - Rebindable!(const Declaration) hasOpEquals; - Rebindable!(const Declaration) hasToHash; - Rebindable!(const Declaration) hasOpCmp; + import dmd.astenums : STC; - // Just return if missing children - if (!structBody || !structBody.declarations || name is Token.init) - return; + if (!(fd.storage_class & STC.disable) && fd.ident.toString() == "opEquals") + hasOpEquals = true; - // Check all the function declarations - foreach (declaration; structBody.declarations) - { - // Skip if not a function declaration - if (!declaration || !declaration.functionDeclaration) - continue; + if (!(fd.storage_class & STC.disable) && fd.ident.toString() == "toHash") + hasToHash = true; + } - bool containsDisable(A)(const A[] attribs) + private void visitAggregate(AST.AggregateDeclaration ad) + { + bool hasOpEquals, hasToHash; + + if (!ad.members) + return; + + foreach(member; *ad.members) + { + if (auto fd = member.isFuncDeclaration()) { - import std.algorithm.searching : canFind; - return attribs.canFind!(a => a.atAttribute !is null && - a.atAttribute.identifier.text == "disable"); + isInteresting(fd, hasOpEquals, hasToHash); + member.accept(this); } - - const isDeclationDisabled = containsDisable(declaration.attributes) || - containsDisable(declaration.functionDeclaration.memberFunctionAttributes); - - if (isDeclationDisabled) - continue; - - // Check if opEquals or toHash - immutable string methodName = declaration.functionDeclaration.name.text; - if (methodName == "opEquals") - hasOpEquals = declaration; - else if (methodName == "toHash") - hasToHash = declaration; - else if (methodName == "opCmp") - hasOpCmp = declaration; + else if (auto scd = member.isStorageClassDeclaration()) + { + foreach (smember; *scd.decl) + { + if (auto fd2 = smember.isFuncDeclaration()) + { + isInteresting(fd2, hasOpEquals, hasToHash); + smember.accept(this); + } + else + smember.accept(this); + } + } + else + member.accept(this); } - // Warn if has opEquals, but not toHash if (hasOpEquals && !hasToHash) { - string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'."; - addErrorMessage( - Message.Diagnostic.from(fileName, name, message), - [ - Message.Diagnostic.from(fileName, hasOpEquals.get, "'opEquals' defined here") - ], - KEY - ); + string message = ad.ident.toString().dup; + message = "'" ~ message ~ "' has method 'opEquals', but not 'toHash'."; + addErrorMessage(cast(ulong) ad.loc.linnum, cast(ulong) ad.loc.charnum, KEY, message); } - // Warn if has toHash, but not opEquals else if (!hasOpEquals && hasToHash) { - string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'."; - addErrorMessage( - Message.Diagnostic.from(fileName, name, message), - [ - Message.Diagnostic.from(fileName, hasToHash.get, "'toHash' defined here") - ], - KEY - ); + string message = ad.ident.toString().dup; + message = "'" ~ message ~ "' has method 'toHash', but not 'opEquals'."; + addErrorMessage(cast(ulong) ad.loc.linnum, cast(ulong) ad.loc.charnum, KEY, message); } } - enum string KEY = "dscanner.suspicious.incomplete_operator_overloading"; + private enum KEY = "dscanner.suspicious.incomplete_operator_overloading"; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.opequals_tohash_check = Check.enabled; - // TODO: test supplemental diagnostics - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ // Success because it has opEquals and toHash class Chimp { @@ -141,8 +124,7 @@ unittest } // Fail on class opEquals - class Rabbit /+ - ^^^^^^ [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'. +/ + class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'. { const bool opEquals(Object a, Object b) { @@ -151,8 +133,7 @@ unittest } // Fail on class toHash - class Kangaroo /+ - ^^^^^^^^ [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'. +/ + class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'. { override const hash_t toHash() { @@ -161,8 +142,7 @@ unittest } // Fail on struct opEquals - struct Tarantula /+ - ^^^^^^^^^ [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'. +/ + struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'. { const bool opEquals(Object a, Object b) { @@ -171,8 +151,7 @@ unittest } // Fail on struct toHash - struct Puma /+ - ^^^^ [warn]: 'Puma' has method 'toHash', but not 'opEquals'. +/ + struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'. { const nothrow @safe hash_t toHash() { @@ -189,4 +168,4 @@ unittest }c, sac); stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/pokemon.d b/src/dscanner/analysis/pokemon.d index 172999cb..a588379b 100644 --- a/src/dscanner/analysis/pokemon.d +++ b/src/dscanner/analysis/pokemon.d @@ -6,11 +6,8 @@ module dscanner.analysis.pokemon; import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; /** * Checks for Pokémon exception handling, i.e. "gotta' catch 'em all". @@ -23,62 +20,27 @@ import dsymbol.scope_ : Scope; * } * --- */ -final class PokemonExceptionCheck : BaseAnalyzer +extern(C++) class PokemonExceptionCheck(AST) : BaseAnalyzerDmd { - enum MESSAGE = "Catching Error or Throwable is almost always a bad idea."; - enum string KEY = "dscanner.suspicious.catch_em_all"; mixin AnalyzerInfo!"exception_check"; + alias visit = BaseAnalyzerDmd.visit; - alias visit = BaseAnalyzer.visit; - - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const LastCatch lc) + override void visit(AST.Catch c) { - addErrorMessage(lc.tokens[0], KEY, MESSAGE); - lc.accept(this); + if (c.type.isTypeIdentifier().ident.toString() == "Error" || + c.type.isTypeIdentifier().ident.toString() == "Throwable") + addErrorMessage(cast(ulong) c.loc.linnum, cast(ulong) c.loc.charnum, + KEY, MESSAGE); } - bool ignoreType = true; - - override void visit(const Catch c) - { - ignoreType = false; - c.type.accept(this); - ignoreType = true; - - c.accept(this); - } - - override void visit(const Type2 type2) - { - if (ignoreType) - return; - - if (type2.type !is null) - { - type2.type.accept(this); - return; - } - - if (type2.typeIdentifierPart.typeIdentifierPart !is null) - { - return; - } - const identOrTemplate = type2.typeIdentifierPart.identifierOrTemplateInstance; - if (identOrTemplate.templateInstance !is null) - { - return; - } - if (identOrTemplate.identifier.text == "Throwable" - || identOrTemplate.identifier.text == "Error") - { - addErrorMessage(identOrTemplate, KEY, MESSAGE); - } - } +private: + enum MESSAGE = "Catching Error or Throwable is almost always a bad idea."; + enum string KEY = "dscanner.suspicious.catch_em_all"; } unittest @@ -87,7 +49,7 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.exception_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testCatch() { try @@ -106,23 +68,15 @@ unittest { } - catch (Error err) /+ - ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ - { - - } - catch (Throwable err) /+ - ^^^^^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ + catch (Error err) // [warn]: Catching Error or Throwable is almost always a bad idea. { } - catch (shared(Error) err) /+ - ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ + catch (Throwable err) // [warn]: Catching Error or Throwable is almost always a bad idea. { } - catch /+ - ^^^^^ [warn]: Catching Error or Throwable is almost always a bad idea. +/ + catch (shared(Error) err) // [warn]: Catching Error or Throwable is almost always a bad idea. { } @@ -130,4 +84,4 @@ unittest }c, sac); stderr.writeln("Unittest for PokemonExceptionCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/properly_documented_public_functions.d b/src/dscanner/analysis/properly_documented_public_functions.d index 5bad77dd..ba7a9894 100644 --- a/src/dscanner/analysis/properly_documented_public_functions.d +++ b/src/dscanner/analysis/properly_documented_public_functions.d @@ -4,15 +4,12 @@ module dscanner.analysis.properly_documented_public_functions; -import dparse.lexer; -import dparse.ast; -import dparse.formatter : astFmt = format; import dscanner.analysis.base; -import dscanner.utils : safeAccess; - import std.format : format; import std.range.primitives; -import std.stdio; +import std.conv : to; +import std.algorithm.searching : canFind, any, find; +import dmd.astcodegen; /** * Requires each public function to contain the following ddoc sections @@ -22,7 +19,7 @@ import std.stdio; - Ddoc params entries without a parameter trigger warnings as well - RETURNS: (except if it's void, only functions) */ -final class ProperlyDocumentedPublicFunctions : BaseAnalyzer +extern(C++) class ProperlyDocumentedPublicFunctions(AST) : BaseAnalyzerDmd { enum string MISSING_PARAMS_KEY = "dscanner.style.doc_missing_params"; enum string MISSING_PARAMS_MESSAGE = "Parameter %s isn't documented in the `Params` section."; @@ -39,268 +36,183 @@ final class ProperlyDocumentedPublicFunctions : BaseAnalyzer enum string MISSING_THROW_MESSAGE = "An instance of `%s` is thrown but not documented in the `Throws` section"; mixin AnalyzerInfo!"properly_documented_public_functions"; + alias visit = BaseAnalyzerDmd.visit; - /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const Module mod) + override void visit(AST.Module m) { - islastSeenVisibilityLabelPublic = true; - mod.accept(this); + super.visit(m); postCheckSeenDdocParams(); } - override void visit(const UnaryExpression decl) + override void visit(AST.Catch c) { - import std.algorithm.searching : canFind; - - const IdentifierOrTemplateInstance iot = safeAccess(decl) - .functionCallExpression.unaryExpression.primaryExpression - .identifierOrTemplateInstance; + import std.algorithm.iteration : filter; + import std.array : array; - Type newNamedType(N)(N name) - { - Type t = new Type; - t.type2 = new Type2; - t.type2.typeIdentifierPart = new TypeIdentifierPart; - t.type2.typeIdentifierPart.identifierOrTemplateInstance = new IdentifierOrTemplateInstance; - t.type2.typeIdentifierPart.identifierOrTemplateInstance.identifier = name; - return t; - } + thrown = thrown.filter!(a => a != to!string(c.type.toChars())).array; + super.visit(c); + } - if (inThrowExpression && decl.newExpression && decl.newExpression.type && - !thrown.canFind!(a => a == decl.newExpression.type)) - { - thrown ~= decl.newExpression.type; - } - // enforce(condition); - if (iot && iot.identifier.text == "enforce") - { - thrown ~= newNamedType(Token(tok!"identifier", "Exception", 0, 0, 0)); - } - else if (iot && iot.templateInstance && iot.templateInstance.identifier.text == "enforce") - { - // enforce!Type(condition); - if (const TemplateSingleArgument tsa = safeAccess(iot.templateInstance) - .templateArguments.templateSingleArgument) - { - thrown ~= newNamedType(tsa.token); - } - // enforce!(Type)(condition); - else if (const NamedTemplateArgumentList tal = safeAccess(iot.templateInstance) - .templateArguments.namedTemplateArgumentList) - { - if (tal.items.length && tal.items[0].type) - thrown ~= tal.items[0].type; - } - } - decl.accept(this); + override void visit(AST.ThrowStatement t) + { + AST.NewExp ne = t.exp.isNewExp(); + if (ne) + thrown ~= to!string(ne.newtype.toChars()); + + super.visit(t); } - override void visit(const Declaration decl) + override void visit(AST.FuncDeclaration d) { - import std.algorithm.searching : any; - import std.algorithm.iteration : map; + nestedFunc++; + scope (exit) + nestedFunc--; - // skip private symbols - enum tokPrivate = tok!"private", - tokProtected = tok!"protected", - tokPackage = tok!"package", - tokPublic = tok!"public"; + import std.stdio : writeln, writefln; + import std.conv : to; + import std.algorithm.searching : canFind, any, find; + import dmd.dsymbol : Visibility; + import dmd.mtype : Type; + import ddoc.comments : parseComment; + import std.algorithm.iteration : map; + import std.array : array; - // Nested funcs for `Throws` - bool decNestedFunc; - if (decl.functionDeclaration) - { - nestedFuncs++; - decNestedFunc = true; - } - scope(exit) + if (d.comment is null || d.fbody is null || d.visibility.kind != Visibility.Kind.public_) { - if (decNestedFunc) - nestedFuncs--; - } - if (nestedFuncs > 1) - { - decl.accept(this); + super.visit(d); return; } - if (decl.attributes.length > 0) + if (nestedFunc == 1) { - const bool isPublic = !decl.attributes.map!`a.attribute`.any!(x => x == tokPrivate || - x == tokProtected || - x == tokPackage); - // recognize label blocks - if (!hasDeclaration(decl)) - islastSeenVisibilityLabelPublic = isPublic; - - if (!isPublic) - return; - } - - if (islastSeenVisibilityLabelPublic || decl.attributes.map!`a.attribute`.any!(x => x == tokPublic)) - { - // Don't complain about non-documented function declarations - if ((decl.functionDeclaration !is null && decl.functionDeclaration.comment.ptr !is null) || - (decl.templateDeclaration !is null && decl.templateDeclaration.comment.ptr !is null) || - decl.mixinTemplateDeclaration !is null || - (decl.classDeclaration !is null && decl.classDeclaration.comment.ptr !is null) || - (decl.structDeclaration !is null && decl.structDeclaration.comment.ptr !is null)) - decl.accept(this); - } - } - - override void visit(const TemplateDeclaration decl) - { - setLastDdocParams(decl.name, decl.comment); - checkDdocParams(decl.templateParameters); - - withinTemplate = true; - scope(exit) withinTemplate = false; - decl.accept(this); - } + thrown.length = 0; + string[] params; - override void visit(const MixinTemplateDeclaration decl) - { - decl.accept(this); - } + if (d.parameters) foreach (p; *d.parameters) + params ~= to!string(p.ident.toString()); - override void visit(const StructDeclaration decl) - { - setLastDdocParams(decl.name, decl.comment); - checkDdocParams(decl.templateParameters); - decl.accept(this); - } + auto comment = setLastDdocParams(d.loc.linnum, d.loc.charnum, to!string(d.comment)); + checkDdocParams(d.loc.linnum, d.loc.charnum, params, null); - override void visit(const ClassDeclaration decl) - { - setLastDdocParams(decl.name, decl.comment); - checkDdocParams(decl.templateParameters); - decl.accept(this); + auto tf = d.type.isTypeFunction(); + if (tf && tf.next != Type.tvoid && d.comment + && !comment.isDitto && !comment.sections.any!(s => s.name == "Returns")) + addErrorMessage(cast(ulong) d.loc.linnum, cast(ulong) d.loc.charnum, + MISSING_RETURNS_KEY, MISSING_RETURNS_MESSAGE); + } + + super.visit(d); + if (nestedFunc == 1) + foreach (t; thrown) + if (!hasThrowSection(to!string(d.comment))) + addErrorMessage(cast(ulong) d.loc.linnum, cast(ulong) d.loc.charnum, + MISSING_THROW_KEY, MISSING_THROW_MESSAGE.format(t)); } - override void visit(const FunctionDeclaration decl) + override void visit(AST.TemplateDeclaration d) { - import std.algorithm.searching : all, any; - import std.array : Appender; + import dmd.dsymbol : Visibility; + import ddoc.comments : parseComment; + import std.algorithm.iteration : map, filter; + import std.algorithm.searching : find, canFind; + import std.array : array; - // ignore header declaration for now - if (!decl.functionBody || (!decl.functionBody.specifiedFunctionBody - && !decl.functionBody.shortenedFunctionBody)) + if (d.comment is null) return; - if (nestedFuncs == 1) - thrown.length = 0; - // detect ThrowExpression only if not nothrow - if (!decl.attributes.any!(a => a.attribute.text == "nothrow")) + // A `template` inside another public `template` declaration will have visibility undefined + // Check that as well as it's part of the public template + if ((d.visibility.kind != Visibility.Kind.public_) + && !(d.visibility.kind == Visibility.Kind.undefined && withinTemplate)) + return; + + if (d.visibility.kind == Visibility.Kind.public_) { - decl.accept(this); - if (nestedFuncs == 1 && !hasThrowSection(decl.comment)) - foreach(t; thrown) - { - Appender!(char[]) app; - astFmt(&app, t); - addErrorMessage(decl.name, MISSING_THROW_KEY, - MISSING_THROW_MESSAGE.format(app.data)); - } + setLastDdocParams(d.loc.linnum, d.loc.charnum, to!string(d.comment)); + withinTemplate = true; + funcParams.length = 0; + templateParams.length = 0; } - if (nestedFuncs == 1) - { - auto comment = setLastDdocParams(decl.name, decl.comment); - checkDdocParams(decl.parameters, decl.templateParameters); - enum voidType = tok!"void"; - if (decl.returnType is null || decl.returnType.type2.builtinType != voidType) - if (!(comment.isDitto || withinTemplate || comment.sections.any!(s => s.name == "Returns"))) - { - import dscanner.analysis.auto_function : AutoFunctionChecker; + foreach (p; *d.origParameters) + if (!canFind(templateParams, to!string(p.ident.toString()))) + templateParams ~= to!string(p.ident.toString()); - const(Token)[] typeRange; - if (decl.returnType !is null) - typeRange = decl.returnType.tokens; - else - typeRange = AutoFunctionChecker.findAutoReturnType(decl); + super.visit(d); - if (!typeRange.length) - typeRange = [decl.name]; - addErrorMessage(typeRange, MISSING_RETURNS_KEY, MISSING_RETURNS_MESSAGE); - } + if (d.visibility.kind == Visibility.Kind.public_) + { + withinTemplate = false; + checkDdocParams(d.loc.linnum, d.loc.charnum, funcParams, templateParams); } } - // remove thrown Type that are caught - override void visit(const TryStatement ts) - { - import std.algorithm.iteration : filter; - import std.algorithm.searching : canFind; + /** + * Look for: foo(T)(T x) + * In that case, T does not have to be documented, because x must be. + */ + override bool visitEponymousMember(AST.TemplateDeclaration d) + { + import ddoc.comments : parseComment; + import std.algorithm.searching : canFind, any, find; + import std.algorithm.iteration : map, filter; import std.array : array; - ts.accept(this); - - if (ts.catches) - thrown = thrown.filter!(a => !ts.catches.catches - .canFind!(b => b.type == a)) - .array; - } + if (!d.members || d.members.length != 1) + return false; + AST.Dsymbol onemember = (*d.members)[0]; + if (onemember.ident != d.ident) + return false; - override void visit(const ThrowExpression ts) - { - const wasInThrowExpression = inThrowExpression; - inThrowExpression = true; - scope (exit) - inThrowExpression = wasInThrowExpression; - ts.accept(this); - inThrowExpression = false; - } - - alias visit = BaseAnalyzer.visit; - -private: - bool islastSeenVisibilityLabelPublic; - bool withinTemplate; - size_t nestedFuncs; - - static struct Function - { - bool active; - Token name; - const(string)[] ddocParams; - bool[string] params; - } - Function lastSeenFun; - - bool inThrowExpression; - const(Type)[] thrown; + if (AST.FuncDeclaration fd = onemember.isFuncDeclaration()) + { + const comment = parseComment(to!string(d.comment), null); + const paramSection = comment.sections.find!(s => s.name == "Params"); + auto tf = fd.type.isTypeFunction(); - // find invalid ddoc parameters (i.e. they don't occur in a function declaration) - void postCheckSeenDdocParams() - { - import std.format : format; + if (tf) + foreach (idx, p; tf.parameterList) + { - if (lastSeenFun.active) - foreach (p; lastSeenFun.ddocParams) - if (p !in lastSeenFun.params) - addErrorMessage(lastSeenFun.name, NON_EXISTENT_PARAMS_KEY, - NON_EXISTENT_PARAMS_MESSAGE.format(p)); + if (!paramSection.empty && + !canFind(paramSection[0].mapping.map!(a => a[0]).array, to!string(p.ident.toString())) && + !canFind(funcParams, to!string(p.ident.toString()))) + funcParams ~= to!string(p.ident.toString()); - lastSeenFun.active = false; - } + lastSeenFun.params[to!string(p.ident.toString())] = true; - bool hasThrowSection(string commentText) - { - import std.algorithm.searching : canFind; - import ddoc.comments : parseComment; + auto ti = p.type.isTypeIdentifier(); + if (ti is null) + continue; - const comment = parseComment(commentText, null); - return comment.isDitto || comment.sections.canFind!(s => s.name == "Throws"); - } + templateParams = templateParams.filter!(a => a != to!string(ti.ident.toString())).array; + lastSeenFun.params[to!string(ti.ident.toString())] = true; + } + return true; + } + + if (AST.AggregateDeclaration ad = onemember.isAggregateDeclaration()) + return true; + + if (AST.VarDeclaration vd = onemember.isVarDeclaration()) + { + if (d.constraint) + return false; + + if (vd._init) + return true; + } + + return false; + } - auto setLastDdocParams(Token name, string commentText) + extern(D) auto setLastDdocParams(size_t line, size_t column, string commentText) { import ddoc.comments : parseComment; import std.algorithm.searching : find; @@ -323,20 +235,28 @@ private: const paramSection = comment.sections.find!(s => s.name == "Params"); if (paramSection.empty) { - lastSeenFun = Function(true, name, null); + lastSeenFun = Function(true, line, column, null); } else { auto ddocParams = paramSection[0].mapping.map!(a => a[0]).array; - lastSeenFun = Function(true, name, ddocParams); + lastSeenFun = Function(true, line, column, ddocParams); } } return comment; } - void checkDdocParams(const Parameters params, - const TemplateParameters templateParameters = null) + /** + * + * Params: + * line = Line of the public declaration verified + * column = Column of the public declaration verified + * params = Funcion parameters that must be documented + * templateParams = Template parameters that must be documented. + * Can be null if we are looking at a regular FuncDeclaration + */ + extern(D) void checkDdocParams(size_t line, size_t column, string[] params, string[] templateParams) { import std.array : array; import std.algorithm.searching : canFind, countUntil; @@ -344,136 +264,73 @@ private: import std.algorithm.mutation : remove; import std.range : indexed, iota; - // convert templateParameters into a string[] for faster access - const(TemplateParameter)[] templateList; - if (const tp = templateParameters) - if (const tpl = tp.templateParameterList) - templateList = tpl.items; - string[] tlList = templateList.map!(a => templateParamName(a).text).array; - - // make a copy of all parameters and remove the seen ones later during the loop - size_t[] unseenTemplates = templateList.length.iota.array; - - if (lastSeenFun.active && params !is null) - foreach (p; params.parameters) + if (lastSeenFun.active && !params.empty) + foreach (p; params) { - string templateName; - if (auto iot = safeAccess(p).type.type2 - .typeIdentifierPart.identifierOrTemplateInstance.unwrap) - { - templateName = iot.identifier.text; - } - else if (auto iot = safeAccess(p).type.type2.type.type2 - .typeIdentifierPart.identifierOrTemplateInstance.unwrap) - { - templateName = iot.identifier.text; - } - - const idx = tlList.countUntil(templateName); - if (idx >= 0) - { - unseenTemplates = unseenTemplates.remove(idx); - tlList = tlList.remove(idx); - // documenting template parameter should be allowed - lastSeenFun.params[templateName] = true; - } - - if (!lastSeenFun.ddocParams.canFind(p.name.text)) - addErrorMessage(p.name, MISSING_PARAMS_KEY, - format(MISSING_PARAMS_MESSAGE, p.name.text)); + if (!lastSeenFun.ddocParams.canFind(p)) + addErrorMessage(line, column, MISSING_PARAMS_KEY, + format(MISSING_PARAMS_MESSAGE, p)); else - lastSeenFun.params[p.name.text] = true; + lastSeenFun.params[p] = true; } - // now check the remaining, not used template parameters - auto unseenTemplatesArr = templateList.indexed(unseenTemplates).array; - checkDdocParams(unseenTemplatesArr); - } - - void checkDdocParams(const TemplateParameters templateParams) - { - if (lastSeenFun.active && templateParams !is null && - templateParams.templateParameterList !is null) - checkDdocParams(templateParams.templateParameterList.items); + checkDdocParams(line, column, templateParams); } - void checkDdocParams(const TemplateParameter[] templateParams) + extern(D) void checkDdocParams(size_t line, size_t column, string[] templateParams) { import std.algorithm.searching : canFind; foreach (p; templateParams) { - const name = templateParamName(p); - assert(name !is Token.init, "Invalid template parameter name."); // this shouldn't happen - if (!lastSeenFun.ddocParams.canFind(name.text)) - addErrorMessage(name, MISSING_PARAMS_KEY, - format(MISSING_TEMPLATE_PARAMS_MESSAGE, name.text)); + if (!lastSeenFun.ddocParams.canFind(p)) + addErrorMessage(line, column, MISSING_PARAMS_KEY, + format(MISSING_TEMPLATE_PARAMS_MESSAGE, p)); else - lastSeenFun.params[name.text] = true; + lastSeenFun.params[p] = true; } } - static Token templateParamName(const TemplateParameter p) + extern(D) bool hasThrowSection(string commentText) + { + import std.algorithm.searching : canFind; + import ddoc.comments : parseComment; + + const comment = parseComment(commentText, null); + return comment.isDitto || comment.sections.canFind!(s => s.name == "Throws"); + } + + void postCheckSeenDdocParams() { - if (p.templateTypeParameter) - return p.templateTypeParameter.identifier; - if (p.templateValueParameter) - return p.templateValueParameter.identifier; - if (p.templateAliasParameter) - return p.templateAliasParameter.identifier; - if (p.templateTupleParameter) - return p.templateTupleParameter.identifier; - if (p.templateThisParameter) - return p.templateThisParameter.templateTypeParameter.identifier; - - return Token.init; + import std.format : format; + + if (lastSeenFun.active) + foreach (p; lastSeenFun.ddocParams) + if (p !in lastSeenFun.params) + addErrorMessage(lastSeenFun.line, lastSeenFun.column, NON_EXISTENT_PARAMS_KEY, + NON_EXISTENT_PARAMS_MESSAGE.format(p)); + + lastSeenFun.active = false; } - bool hasDeclaration(const Declaration decl) + private enum KEY = "dscanner.performance.enum_array_literal"; + int nestedFunc; + int withinTemplate; + + extern(D) string[] funcParams; + extern(D) string[] templateParams; + extern(D) string[] thrown; + + static struct Function { - import std.meta : AliasSeq; - alias properties = AliasSeq!( - "aliasDeclaration", - "aliasThisDeclaration", - "anonymousEnumDeclaration", - "attributeDeclaration", - "classDeclaration", - "conditionalDeclaration", - "constructor", - "debugSpecification", - "destructor", - "enumDeclaration", - "eponymousTemplateDeclaration", - "functionDeclaration", - "importDeclaration", - "interfaceDeclaration", - "invariant_", - "mixinDeclaration", - "mixinTemplateDeclaration", - "postblit", - "pragmaDeclaration", - "sharedStaticConstructor", - "sharedStaticDestructor", - "staticAssertDeclaration", - "staticConstructor", - "staticDestructor", - "structDeclaration", - "templateDeclaration", - "unionDeclaration", - "unittest_", - "variableDeclaration", - "versionSpecification", - ); - if (decl.declarations !is null) - return false; - - auto isNull = true; - foreach (property; properties) - if (mixin("decl." ~ property ~ " !is null")) - isNull = false; - - return !isNull; + bool active; + size_t line, column; + // All params documented + const(string)[] ddocParams; + // Stores actual function params that are also documented + bool[string] params; } + Function lastSeenFun; } version(unittest) @@ -481,7 +338,7 @@ version(unittest) import std.stdio : stderr; import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; } // missing params @@ -494,73 +351,68 @@ unittest /** Some text */ - void foo(int k){} /+ - ^ [warn]: %s +/ + void foo(int k){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_PARAMS_MESSAGE.format("k") + ), sac, true); assertAnalyzerWarnings(q{ /** Some text */ - void foo(int K)(){} /+ - ^ [warn]: %s +/ + void foo(int K)(){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("K") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("K") + ), sac, true); assertAnalyzerWarnings(q{ /** Some text */ - struct Foo(Bar){} /+ - ^^^ [warn]: %s +/ + struct Foo(Bar){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") + ), sac, true); assertAnalyzerWarnings(q{ /** Some text */ - class Foo(Bar){} /+ - ^^^ [warn]: %s +/ + class Foo(Bar){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") + ), sac, true); assertAnalyzerWarnings(q{ /** Some text */ - template Foo(Bar){} /+ - ^^^ [warn]: %s +/ + template Foo(Bar){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") + ), sac, true); // test no parameters assertAnalyzerWarnings(q{ /** Some text */ void foo(){} - }c, sac); + }c, sac, true); assertAnalyzerWarnings(q{ /** Some text */ struct Foo(){} - }c, sac); + }c, sac, true); assertAnalyzerWarnings(q{ /** Some text */ class Foo(){} - }c, sac); + }c, sac, true); assertAnalyzerWarnings(q{ /** Some text */ template Foo(){} - }c, sac); + }c, sac, true); } @@ -574,21 +426,19 @@ unittest /** Some text */ - int foo(){} /+ - ^^^ [warn]: %s +/ + int foo(){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_RETURNS_MESSAGE, + ), sac, true); assertAnalyzerWarnings(q{ /** Some text */ - auto foo(){} /+ - ^^^^ [warn]: %s +/ + auto foo(){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_RETURNS_MESSAGE, + ), sac, true); } // ignore private @@ -602,7 +452,7 @@ unittest Some text */ private void foo(int k){} - }c, sac); + }c, sac, true); // with block assertAnalyzerWarnings(q{ @@ -612,16 +462,14 @@ unittest */ private void foo(int k){} /// - public int bar(){} /+ - ^^^ [warn]: %s +/ + public int bar(){ return 0; } // [warn]: %s public: /// - int foobar(){} /+ - ^^^ [warn]: %s +/ + int foobar(){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, - ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_RETURNS_MESSAGE, + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_RETURNS_MESSAGE, + ), sac, true); // with block (template) assertAnalyzerWarnings(q{ @@ -631,16 +479,14 @@ unittest */ private template foo(int k){} /// - public template bar(T){} /+ - ^ [warn]: %s +/ + public template bar(T){} // [warn]: %s public: /// - template foobar(T){} /+ - ^ [warn]: %s +/ + template foobar(T){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), + ), sac, true); // with block (struct) assertAnalyzerWarnings(q{ @@ -650,16 +496,14 @@ unittest */ private struct foo(int k){} /// - public struct bar(T){} /+ - ^ [warn]: %s +/ + public struct bar(T){} // [warn]: %s public: /// - struct foobar(T){} /+ - ^ [warn]: %s +/ + struct foobar(T){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), + ), sac, true); } // test parameter names @@ -677,11 +521,10 @@ unittest * Returns: * A long description. */ -int foo(int k){} /+ - ^ [warn]: %s +/ +int foo(int k){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_PARAMS_MESSAGE.format("k") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -692,11 +535,10 @@ int foo(int k){} /+ * Returns: * A long description. */ -int foo(int k) => k; /+ - ^ [warn]: %s +/ +int foo(int k) => k; // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_PARAMS_MESSAGE.format("k") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -709,11 +551,10 @@ k = A stupid parameter Returns: A long description. */ -int foo(int k){} /+ - ^^^ [warn]: %s +/ +int foo(int k){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("val") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).NON_EXISTENT_PARAMS_MESSAGE.format("val") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -724,11 +565,10 @@ Params: Returns: A long description. */ -int foo(int k){} /+ - ^ [warn]: %s +/ +int foo(int k){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_PARAMS_MESSAGE.format("k") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -742,11 +582,10 @@ foobar = A stupid parameter Returns: A long description. */ -int foo(int foo, int foobar){} /+ - ^^^ [warn]: %s +/ +int foo(int foo, int foobar){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).NON_EXISTENT_PARAMS_MESSAGE.format("bad") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -760,11 +599,10 @@ foobar = A stupid parameter Returns: A long description. */ -struct foo(int foo, int foobar){} /+ - ^^^ [warn]: %s +/ +struct foo(int foo, int foobar){} // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).NON_EXISTENT_PARAMS_MESSAGE.format("bad") + ), sac, true); // properly documented assertAnalyzerWarnings(q{ @@ -778,8 +616,8 @@ bar = A stupid parameter Returns: A long description. */ -int foo(int foo, int bar){} - }c, sac); +int foo(int foo, int bar){ return 0; } + }c, sac, true); assertAnalyzerWarnings(q{ /** @@ -793,7 +631,7 @@ Returns: A long description. */ struct foo(int foo, int bar){} - }c, sac); + }c, sac, true); } // support ditto @@ -812,11 +650,11 @@ unittest * Returns: * A long description. */ -int foo(int k){} +int foo(int k){ return 0; } /// ditto -int bar(int k){} - }c, sac); +int bar(int k){ return 0; } + }c, sac, true); assertAnalyzerWarnings(q{ /** @@ -829,11 +667,11 @@ int bar(int k){} * Returns: * A long description. */ -int foo(int k){} +int foo(int k){ return 0; } /// ditto struct Bar(K){} - }c, sac); + }c, sac, true); assertAnalyzerWarnings(q{ /** @@ -846,11 +684,11 @@ struct Bar(K){} * Returns: * A long description. */ -int foo(int k){} +int foo(int k){ return 0; } /// ditto -int bar(int f){} - }c, sac); +int bar(int f){ return 0; } + }c, sac, true); assertAnalyzerWarnings(q{ /** @@ -862,14 +700,13 @@ int bar(int f){} * Returns: * A long description. */ -int foo(int k){} +int foo(int k){ return 0; } /// ditto -int bar(int bar){} /+ - ^^^ [warn]: %s +/ +int bar(int bar){ return 0; } // [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("bar") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_PARAMS_MESSAGE.format("bar") + ), sac, true); assertAnalyzerWarnings(q{ /** @@ -885,14 +722,13 @@ int bar(int bar){} /+ * See_Also: * $(REF takeExactly, std,range) */ -int foo(int k){} /+ - ^^^ [warn]: %s +/ +int foo(int k){ return 0; } // [warn]: %s /// ditto -int bar(int bar){} +int bar(int bar){ return 0; } }c.format( - ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("f") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).NON_EXISTENT_PARAMS_MESSAGE.format("f") + ), sac, true); } // check correct ddoc headers @@ -912,8 +748,8 @@ unittest Returns: Awesome values. +/ -string bar(string val){} - }c, sac); +string bar(string val){ return ""; } + }c, sac, true); assertAnalyzerWarnings(q{ /++ @@ -927,7 +763,7 @@ string bar(string val){} Returns: Awesome values. +/ template bar(string val){} - }c, sac); + }c, sac, true); } @@ -958,7 +794,7 @@ template abcde(Args ...) { /// .... } } - }c, sac); + }c, sac, true); } // Don't force the documentation of the template parameter if it's a used type in the parameter list @@ -977,7 +813,7 @@ Params: Returns: Awesome values. +/ string bar(R)(R r){} - }c, sac); + }c, sac, true); assertAnalyzerWarnings(q{ /++ @@ -988,11 +824,10 @@ Params: Returns: Awesome values. +/ -string bar(P, R)(R r){}/+ - ^ [warn]: %s +/ +string bar(P, R)(R r){}// [warn]: %s }c.format( - ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("P") - ), sac); + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_TEMPLATE_PARAMS_MESSAGE.format("P") + ), sac, true); } // https://github.com/dlang-community/D-Scanner/issues/601 @@ -1007,7 +842,7 @@ unittest alias p = put!(Unqual!Range); p(items); } - }, sac); + }, sac, true); } unittest @@ -1026,7 +861,7 @@ unittest +/ void put(Range)(const(Range) items) if (canPutConstRange!Range) {} - }, sac); + }, sac, true); } unittest @@ -1035,214 +870,75 @@ unittest sac.properly_documented_public_functions = Check.enabled; assertAnalyzerWarnings(q{ +class AssertError : Error +{ + this(string msg) { super(msg); } +} + /++ Throw but likely catched. +/ -void bar(){ +void bar1(){ try{throw new Exception("bla");throw new Error("bla");} catch(Exception){} catch(Error){}} - }c, sac); -} -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ Simple case +/ -void bar(){throw new Exception("bla");} /+ - ^^^ [warn]: %s +/ - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("Exception") - ), sac); -} + void bar2(){throw new Exception("bla");} // [warn]: %s -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ Supposed to be documented Throws: Exception if... +/ -void bar(){throw new Exception("bla");} - }c.format( - ), sac); -} +void bar3(){throw new Exception("bla");} -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ rethrow +/ -void bar(){try throw new Exception("bla"); catch(Exception) throw new Error();} /+ - ^^^ [warn]: %s +/ - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("Error") - ), sac); -} +void bar4(){try throw new Exception("bla"); catch(Exception) throw new Error("bla");} // [warn]: %s -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ trust nothrow before everything +/ -void bar() nothrow {try throw new Exception("bla"); catch(Exception) assert(0);} - }c, sac); -} - -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; +void bar5() nothrow {try throw new Exception("bla"); catch(Exception) assert(0);} - assertAnalyzerWarnings(q{ /++ case of throw in nested func +/ -void bar() /+ - ^^^ [warn]: %s +/ +void bar6() // [warn]: %s { void foo(){throw new AssertError("bla");} foo(); } - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("AssertError") - ), sac); -} -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ case of throw in nested func but caught +/ -void bar() +void bar7() { void foo(){throw new AssertError("bla");} try foo(); catch (AssertError){} } - }c, sac); -} -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ /++ case of double throw in nested func but only 1 caught +/ -void bar() /+ - ^^^ [warn]: %s +/ +void bar8() // [warn]: %s { void foo(){throw new AssertError("bla");throw new Error("bla");} try foo(); catch (Error){} -} - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("AssertError") - ), sac); -} - -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ -/++ -enforce -+/ -void bar() /+ - ^^^ [warn]: %s +/ -{ - enforce(condition); -} - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("Exception") - ), sac); -} - -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ -/++ -enforce -+/ -void bar() /+ - ^^^ [warn]: %s +/ -{ - enforce!AssertError(condition); -} - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("AssertError") - ), sac); -} - -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ -/++ -enforce -+/ -void bar() /+ - ^^^ [warn]: %s +/ -{ - enforce!(AssertError)(condition); -} - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("AssertError") - ), sac); -} - -unittest -{ - StaticAnalysisConfig sac = disabledConfig; - sac.properly_documented_public_functions = Check.enabled; - - assertAnalyzerWarnings(q{ -/++ -enforce -+/ -void foo() /+ - ^^^ [warn]: %s +/ -{ - void bar() - { - enforce!AssertError(condition); - } - bar(); -} - - }c.format( - ProperlyDocumentedPublicFunctions.MISSING_THROW_MESSAGE.format("AssertError") - ), sac); +}}c.format( + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_THROW_MESSAGE.format("object.Exception"), + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_THROW_MESSAGE.format("object.Error"), + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_THROW_MESSAGE + .format("properly_documented_public_functions.AssertError"), + (ProperlyDocumentedPublicFunctions!ASTCodegen).MISSING_THROW_MESSAGE + .format("properly_documented_public_functions.AssertError") + ), sac, true); } // https://github.com/dlang-community/D-Scanner/issues/583 @@ -1254,10 +950,8 @@ unittest assertAnalyzerWarnings(q{ /++ Implements the homonym function (also known as `accumulate`) - Returns: the accumulated `result` - Params: fun = one or more functions +/ @@ -1266,17 +960,15 @@ unittest { /++ No-seed version. The first element of `r` is used as the seed's value. - Params: r = an iterable value as defined by `isIterable` - Returns: the final result of the accumulator applied to the iterable +/ auto reduce(R)(R r){} } }c.format( - ), sac); + ), sac, true); stderr.writeln("Unittest for ProperlyDocumentedPublicFunctions passed."); } diff --git a/src/dscanner/analysis/range.d b/src/dscanner/analysis/range.d index a60f13e1..2790787d 100644 --- a/src/dscanner/analysis/range.d +++ b/src/dscanner/analysis/range.d @@ -6,20 +6,17 @@ module dscanner.analysis.range; import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; +import std.string : format; /** * Checks for .. expressions where the left side is larger than the right. This * is almost always a mistake. */ -final class BackwardsRangeCheck : BaseAnalyzer +extern(C++) class BackwardsRangeCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"backwards_range_check"; /// Key for this check in the report output @@ -29,131 +26,37 @@ final class BackwardsRangeCheck : BaseAnalyzer * Params: * fileName = the name of the file being analyzed */ - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const ForeachStatement foreachStatement) - { - if (foreachStatement.low !is null && foreachStatement.high !is null) - { - import std.string : format; - - state = State.left; - visit(foreachStatement.low); - state = State.right; - visit(foreachStatement.high); - state = State.ignore; - if (hasLeft && hasRight && left > right) - { - string message = format( - "%d is larger than %d. Did you mean to use 'foreach_reverse( ... ; %d .. %d)'?", - left, right, right, left); - auto start = &foreachStatement.low.tokens[0]; - auto endIncl = &foreachStatement.high.tokens[$ - 1]; - assert(endIncl >= start); - auto tokens = start[0 .. endIncl - start + 1]; - addErrorMessage(tokens, KEY, message); - } - hasLeft = false; - hasRight = false; - } - foreachStatement.accept(this); - } - - override void visit(const AddExpression add) - { - immutable s = state; - state = State.ignore; - add.accept(this); - state = s; - } - - override void visit(const UnaryExpression unary) + extern(D) this(string fileName, bool skipTests = false) { - if (state != State.ignore && unary.primaryExpression is null) - return; - else - unary.accept(this); + super(fileName, skipTests); } - override void visit(const PrimaryExpression primary) + override void visit(AST.IntervalExp ie) { - import std.conv : to, ConvException; + auto lwr = ie.lwr.isIntegerExp(); + auto upr = ie.upr.isIntegerExp(); - if (state == State.ignore || !isNumberLiteral(primary.primary.type)) - return; - if (state == State.left) + if (lwr && upr && lwr.getInteger() > upr.getInteger()) { - try - left = parseNumber(primary.primary.text); - catch (ConvException e) - return; - hasLeft = true; - } - else - { - try - right = parseNumber(primary.primary.text); - catch (ConvException e) - return; - hasRight = true; + string message = format("%d is larger than %d. This slice is likely incorrect.", + lwr.getInteger(), upr.getInteger()); + addErrorMessage(cast(ulong) ie.loc.linnum, cast(ulong) ie.loc.charnum, KEY, message); } + } - override void visit(const Index index) + override void visit(AST.ForeachRangeStatement s) { - if (index.low !is null && index.high !is null) - { - state = State.left; - dynamicDispatch(index.low); - state = State.right; - dynamicDispatch(index.high); - state = State.ignore; - if (hasLeft && hasRight && left > right) - { - import std.string : format; + auto lwr = s.lwr.isIntegerExp(); + auto upr = s.upr.isIntegerExp(); - string message = format("%d is larger than %d. This slice is likely incorrect.", - left, right); - addErrorMessage(index, KEY, message); - } - hasLeft = false; - hasRight = false; - } - index.accept(this); - } - -private: - bool hasLeft; - bool hasRight; - long left; - long right; - enum State - { - ignore, - left, - right - } - - State state = State.ignore; - - long parseNumber(string te) - { - import std.conv : to; - import std.regex : ctRegex, replaceAll; - - enum re = ctRegex!("[_uUlL]", ""); - string t = te.replaceAll(re, ""); - if (t.length > 2) + if (lwr && upr && lwr.getInteger() > upr.getInteger()) { - if (t[1] == 'x' || t[1] == 'X') - return to!long(t[2 .. $], 16); - if (t[1] == 'b' || t[1] == 'B') - return to!long(t[2 .. $], 2); + string message = format( + "%d is larger than %d. Did you mean to use 'foreach_reverse( ... ; %d .. %d)'?", + lwr.getInteger(), upr.getInteger(), upr.getInteger(), lwr.getInteger()); + addErrorMessage(cast(ulong) s.loc.linnum, cast(ulong) s.loc.charnum, KEY, message); } - return to!long(t); } } @@ -163,7 +66,7 @@ unittest StaticAnalysisConfig sac = disabledConfig(); sac.backwards_range_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testRange() { a = node.tupleof[2..T.length+1]; // ok @@ -172,12 +75,10 @@ unittest int[] data = [1, 2, 3, 4, 5]; data = data[1 .. 3]; // ok - data = data[3 .. 1]; /+ - ^^^^^^ [warn]: 3 is larger than 1. This slice is likely incorrect. +/ + data = data[3 .. 1]; // [warn]: 3 is larger than 1. This slice is likely incorrect. foreach (n; 1 .. 3) { } // ok - foreach (n; 3 .. 1) { } /+ - ^^^^^^ [warn]: 3 is larger than 1. Did you mean to use 'foreach_reverse( ... ; 1 .. 3)'? +/ + foreach (n; 3 .. 1) { } // [warn]: 3 is larger than 1. Did you mean to use 'foreach_reverse( ... ; 1 .. 3)'? } }c, sac); diff --git a/src/dscanner/analysis/redundant_attributes.d b/src/dscanner/analysis/redundant_attributes.d index 34a3cb3a..0dce40a9 100644 --- a/src/dscanner/analysis/redundant_attributes.d +++ b/src/dscanner/analysis/redundant_attributes.d @@ -4,156 +4,60 @@ module dscanner.analysis.redundant_attributes; -import dparse.ast; -import dparse.lexer; -import dsymbol.scope_ : Scope; import dscanner.analysis.base; import dscanner.analysis.helpers; -import std.algorithm; -import std.conv : to, text; -import std.range : empty, front, walkLength; +import dmd.dsymbol; +import std.string : format; /** * Checks for redundant attributes. At the moment only visibility attributes. */ -final class RedundantAttributesCheck : ScopedBaseAnalyzer +extern(C++) class RedundantAttributesCheck(AST) : BaseAnalyzerDmd { mixin AnalyzerInfo!"redundant_attributes_check"; + alias visit = BaseAnalyzerDmd.visit; + + Visibility.Kind currVisibility; + uint currLine; - this(BaseAnalyzerArguments args) + extern(D) this(string fileName) { - super(args); - stack.length = 0; + super(fileName); } - override void visit(const Declaration decl) + template ScopedVisit(NodeType) { - - // labels, e.g. private: - if (auto attr = decl.attributeDeclaration) - { - if (filterAttributes(attr.attribute)) - { - addAttribute(attr.attribute); - } - } - - auto attributes = decl.attributes.filter!(a => filterAttributes(a)); - if (attributes.walkLength > 0) { - - // new scope: private { } - if (decl.declarations.length > 0) - { - const prev = currentAttributes[]; - // append to current scope and reset once block is left - foreach (attr; attributes) - addAttribute(attr); - - scope(exit) currentAttributes = prev; - decl.accept(this); - } // declarations, e.g. private int ... - else - { - foreach (attr; attributes) - checkAttribute(attr); - - decl.accept(this); - } - } - else - { - decl.accept(this); - } - } - - alias visit = ScopedBaseAnalyzer.visit; - - mixin ScopedVisit!ConditionalDeclaration; - -private: - - alias ConstAttribute = const Attribute; - alias CurrentScope = ConstAttribute[]; - ref CurrentScope currentAttributes() - { - return stack[$ - 1]; - } - - CurrentScope[] stack; - - void addAttribute(const Attribute attr) - { - removeOverwrite(attr); - if (checkAttribute(attr)) - { - currentAttributes ~= attr; - } - } - - bool checkAttribute(const Attribute attr) - { - auto match = currentAttributes.find!(a => a.attribute.type == attr.attribute.type); - if (!match.empty) - { - addErrorMessage(attr, KEY, - text("same visibility attribute used as defined on line ", - match.front.attribute.line.to!string, ".")); - return false; - } - return true; - } - - void removeOverwrite(const Attribute attr) - { - import std.array : array; - const group = getAttributeGroup(attr); - if (currentAttributes.filter!(a => getAttributeGroup(a) == group - && !isIdenticalAttribute(a, attr)).walkLength > 0) + override void visit(NodeType n) { - currentAttributes = currentAttributes.filter!(a => getAttributeGroup(a) != group - || isIdenticalAttribute(a, attr)).array; + Visibility.Kind prevVisibility = currVisibility; + currVisibility = Visibility.Kind.undefined; + super.visit(n); + currVisibility = prevVisibility; } } - bool filterAttributes(const Attribute attr) - { - return isAccessSpecifier(attr); - } - - static int getAttributeGroup(const Attribute attr) - { - if (isAccessSpecifier(attr)) - return 1; - - // TODO: not implemented - return attr.attribute.type; - } - - static bool isAccessSpecifier(const Attribute attr) - { - auto type = attr.attribute.type; - return type.among(tok!"private", tok!"protected", tok!"public", tok!"package", tok!"export") > 0; - } - - static bool isIdenticalAttribute(const Attribute a, const Attribute b) - { - return a.attribute.type == b.attribute.type; - } - - auto attributesString() - { - return currentAttributes.map!(a => a.attribute.type.str).joiner(",").to!string; - } - - protected override void pushScope() - { - stack.length++; - } + mixin ScopedVisit!(AST.StructDeclaration); + mixin ScopedVisit!(AST.ClassDeclaration); + mixin ScopedVisit!(AST.InterfaceDeclaration); + mixin ScopedVisit!(AST.UnionDeclaration); + mixin ScopedVisit!(AST.StaticIfCondition); + mixin ScopedVisit!(AST.StaticIfDeclaration); + mixin ScopedVisit!(AST.TemplateDeclaration); + mixin ScopedVisit!(AST.ConditionalDeclaration); - protected override void popScope() + override void visit(AST.VisibilityDeclaration vd) { - stack.length--; + if (currVisibility == vd.visibility.kind) + addErrorMessage(cast(ulong) vd.loc.linnum, cast(ulong) vd.loc.charnum, KEY, + "Same visibility attribute used as defined on line %u.".format(currLine)); + Visibility.Kind prevVisibility = currVisibility; + uint prevLine = currLine; + currVisibility = vd.visibility.kind; + currLine = vd.loc.linnum; + super.visit(vd); + currVisibility = prevVisibility; + currLine = prevLine; } enum string KEY = "dscanner.suspicious.redundant_attributes"; @@ -172,82 +76,72 @@ unittest sac.redundant_attributes_check = Check.enabled; // test labels vs. block attributes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { private: - private int blah; /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ + private int blah; // [warn]: Same visibility attribute used as defined on line 4. protected { - protected int blah; /+ - ^^^^^^^^^ [warn]: same visibility attribute used as defined on line 7. +/ + protected int blah; // [warn]: Same visibility attribute used as defined on line 6. } - private int blah; /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ + private int blah; // [warn]: Same visibility attribute used as defined on line 4. }}c, sac); // test labels vs. block attributes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { private: - private: /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ + private: // [warn]: Same visibility attribute used as defined on line 4. public: private int a; - public int b; /+ - ^^^^^^ [warn]: same visibility attribute used as defined on line 7. +/ - public /+ - ^^^^^^ [warn]: same visibility attribute used as defined on line 7. +/ + public int b; // [warn]: Same visibility attribute used as defined on line 6. + public // [warn]: Same visibility attribute used as defined on line 6. { int c; } }}c, sac); // test scopes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { private: - private int foo2; /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ - private void foo() /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ + private int foo2; // [warn]: Same visibility attribute used as defined on line 4. + private void foo() // [warn]: Same visibility attribute used as defined on line 4. { - private int blah; + int blah; } }}c, sac); // check duplicated visibility attributes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { private: public int a; -private: /+ -^^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ +private: // [warn]: Same visibility attribute used as defined on line 4. }}c, sac); // test conditional compilation - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { version(unittest) { private: - private int foo; /+ - ^^^^^^^ [warn]: same visibility attribute used as defined on line 6. +/ + private int foo; // [warn]: Same visibility attribute used as defined on line 6. } private int foo2; }}c, sac); // test scopes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { public: - if (1 == 1) + static if (1 == 1) { private int b; } @@ -255,8 +149,7 @@ public: { public int b; } - public int b; /+ - ^^^^^^ [warn]: same visibility attribute used as defined on line 4. +/ + public int b; // [warn]: Same visibility attribute used as defined on line 4. }}c, sac); } @@ -267,8 +160,8 @@ unittest sac.redundant_attributes_check = Check.enabled; // test labels vs. block attributes - assertAnalyzerWarnings(q{ -unittest + assertAnalyzerWarningsDMD(q{ +class C { @safe: @safe void foo(); diff --git a/src/dscanner/analysis/redundant_parens.d b/src/dscanner/analysis/redundant_parens.d index a541c175..a0863c5b 100644 --- a/src/dscanner/analysis/redundant_parens.d +++ b/src/dscanner/analysis/redundant_parens.d @@ -5,60 +5,63 @@ module dscanner.analysis.redundant_parens; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; /** * Checks for redundant parenthesis */ -final class RedundantParenCheck : BaseAnalyzer +extern (C++) class RedundantParenCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"redundant_parens_check"; /// - this(BaseAnalyzerArguments args) - { - super(args); - } - - override void visit(const IfStatement statement) + extern (D) this(string fileName, bool skipTests = false) { - UnaryExpression unary; - if (statement.condition.expression is null || statement.condition.expression.items.length != 1) - goto end; - unary = cast(UnaryExpression) statement.condition.expression.items[0]; - if (unary is null) - goto end; - if (unary.primaryExpression is null) - goto end; - if (unary.primaryExpression.expression is null) - goto end; - addErrorMessage(unary.primaryExpression, KEY, "Redundant parenthesis."); - end: - statement.accept(this); + super(fileName, skipTests); } - override void visit(const PrimaryExpression primaryExpression) + override void visit(AST.IfStatement s) { - UnaryExpression unary; - if (primaryExpression.expression is null) - goto end; - unary = cast(UnaryExpression) primaryExpression.expression.items[0]; - if (unary is null) - goto end; - if (unary.primaryExpression is null) - goto end; - if (unary.primaryExpression.expression is null) - goto end; - addErrorMessage(primaryExpression, KEY, "Redundant parenthesis."); - end: - primaryExpression.accept(this); + if (s.condition.parens) + addErrorMessage(cast(ulong) s.loc.linnum, cast(ulong) s.loc.charnum, KEY, MESSAGE); } private: enum string KEY = "dscanner.suspicious.redundant_parens"; + enum string MESSAGE = "Redundant parenthesis."; +} + +unittest +{ + import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; + + StaticAnalysisConfig sac = disabledConfig(); + sac.redundant_parens_check = Check.enabled; + + assertAnalyzerWarningsDMD(q{ + void testRedundantParens() + { + int a = 0; + bool b = true; + + if ((a + 2 == 3)) // [warn]: Redundant parenthesis. + { + + } + + if ((b)) // [warn]: Redundant parenthesis. + { + + } + + if (b) { } + + if (a * 2 == 0) { } + } + }c, sac); + + stderr.writeln("Unittest for RedundantParenthesis passed."); } diff --git a/src/dscanner/analysis/redundant_storage_class.d b/src/dscanner/analysis/redundant_storage_class.d index 2570c566..a320cc9e 100644 --- a/src/dscanner/analysis/redundant_storage_class.d +++ b/src/dscanner/analysis/redundant_storage_class.d @@ -5,76 +5,68 @@ module dscanner.analysis.redundant_storage_class; -import std.stdio; import std.string; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dsymbol.scope_ : Scope; /** * Checks for redundant storage classes such immutable and __gshared, static and __gshared */ -final class RedundantStorageClassCheck : BaseAnalyzer +extern (C++) class RedundantStorageClassCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - enum string REDUNDANT_VARIABLE_ATTRIBUTES = "Variable declaration for `%s` has redundant attributes (%-(`%s`%|, %))."; + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"redundant_storage_classes"; - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum KEY = "dscanner.unnecessary.duplicate_attribute"; + private enum string REDUNDANT_VARIABLE_ATTRIBUTES = "Variable declaration for `%s` has redundant attributes (%-(`%s`%|, %))."; - override void visit(const Declaration node) + extern (D) this(string fileName, bool skipTests = false) { - checkAttributes(node); - node.accept(this); + super(fileName, skipTests); } - void checkAttributes(const Declaration node) + override void visit(AST.VarDeclaration varDecl) { - if (node.variableDeclaration !is null && node.attributes !is null) - checkVariableDeclaration(node.variableDeclaration, node.attributes); + import dmd.astenums : STC; + + if (varDecl.storage_class & STC.immutable_ && varDecl.storage_class & STC.shared_) + addErrorFor(varDecl, "immutable", "shared"); + + if (varDecl.storage_class & STC.immutable_ && varDecl.storage_class & STC.gshared) + addErrorFor(varDecl, "immutable", "__gshared"); + + if (varDecl.storage_class & STC.static_ && varDecl.storage_class & STC.gshared) + addErrorFor(varDecl, "static", "__gshared"); } - void checkVariableDeclaration(const VariableDeclaration vd, const Attribute[] attributes) + extern (D) private void addErrorFor(AST.VarDeclaration varDecl, string attr1, string attr2) { - import std.algorithm.comparison : among; - import std.algorithm.searching: all; - - string[] globalAttributes; - foreach (attrib; attributes) - { - if (attrib.attribute.type.among(tok!"shared", tok!"static", tok!"__gshared", tok!"immutable")) - globalAttributes ~= attrib.attribute.type.str; - } - if (globalAttributes.length > 1) - { - if (globalAttributes.length == 2 && ( - globalAttributes.all!(a => a.among("shared", "static")) || - globalAttributes.all!(a => a.among("static", "immutable")) - )) - return; - auto t = vd.declarators[0].name; - string message = REDUNDANT_VARIABLE_ATTRIBUTES.format(t.text, globalAttributes); - addErrorMessage(t, KEY, message); - } + auto lineNum = cast(size_t) varDecl.loc.linnum; + auto charNum = cast(size_t) varDecl.loc.charnum; + auto varName = varDecl.ident.toString(); + auto errorMsg = REDUNDANT_VARIABLE_ATTRIBUTES.format(varName, [ + attr1, attr2 + ]); + addErrorMessage(lineNum, charNum, KEY, errorMsg); } - - private enum string KEY = "dscanner.unnecessary.duplicate_attribute"; } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; + import std.format : format; StaticAnalysisConfig sac = disabledConfig(); sac.redundant_storage_classes = Check.enabled; + enum string erorMsg = "Variable declaration for `%s` has redundant attributes (%-(`%s`%|, %))."; + auto immutableSharedMsg = erorMsg.format("a", ["immutable", "shared"]); + auto immutableGSharedMsg = erorMsg.format("a", ["immutable", "__gshared"]); + auto staticGSharedMsg = erorMsg.format("a", ["static", "__gshared"]); + // https://github.com/dlang-community/D-Scanner/issues/438 - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ immutable int a; immutable shared int a; // [warn]: %s @@ -93,13 +85,8 @@ unittest enum int a; extern(C++) immutable int a; immutable int function(immutable int, shared int) a; - }c.format( - RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["immutable", "shared"]), - RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["shared", "immutable"]), - RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["immutable", "__gshared"]), - RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["__gshared", "immutable"]), - RedundantStorageClassCheck.REDUNDANT_VARIABLE_ATTRIBUTES.format("a", ["__gshared", "static"]), - ), sac); + }c.format(immutableSharedMsg, immutableSharedMsg, immutableGSharedMsg, + immutableGSharedMsg, staticGSharedMsg), sac); stderr.writeln("Unittest for RedundantStorageClassCheck passed."); } diff --git a/src/dscanner/analysis/run.d b/src/dscanner/analysis/run.d index 7965135d..eba165c3 100644 --- a/src/dscanner/analysis/run.d +++ b/src/dscanner/analysis/run.d @@ -11,9 +11,9 @@ import dparse.ast; import dparse.lexer; import dparse.parser; import dparse.rollback_allocator; + import std.algorithm; import std.array; -import std.array; import std.conv; import std.file : mkdirRecurse; import std.functional : toDelegate; @@ -22,18 +22,14 @@ import std.range; import std.stdio; import std.typecons : scoped; -import std.experimental.allocator : CAllocatorImpl; -import std.experimental.allocator.mallocator : Mallocator; -import std.experimental.allocator.building_blocks.region : Region; -import std.experimental.allocator.building_blocks.allocator_list : AllocatorList; - +import dscanner.analysis.autofix : improveAutoFixWhitespace; import dscanner.analysis.config; import dscanner.analysis.base; +import dscanner.analysis.rundmd; import dscanner.analysis.style; import dscanner.analysis.enumarrayliteral; import dscanner.analysis.pokemon; import dscanner.analysis.del; -import dscanner.analysis.fish; import dscanner.analysis.numbers; import dscanner.analysis.objectconst; import dscanner.analysis.range; @@ -42,7 +38,6 @@ import dscanner.analysis.constructors; import dscanner.analysis.unused_variable; import dscanner.analysis.unused_label; import dscanner.analysis.unused_parameter; -import dscanner.analysis.duplicate_attribute; import dscanner.analysis.opequals_without_tohash; import dscanner.analysis.length_subtraction; import dscanner.analysis.builtin_property_names; @@ -50,11 +45,9 @@ import dscanner.analysis.asm_style; import dscanner.analysis.logic_precedence; import dscanner.analysis.stats_collector; import dscanner.analysis.undocumented; -import dscanner.analysis.comma_expression; import dscanner.analysis.function_attributes; import dscanner.analysis.local_imports; import dscanner.analysis.unmodified; -import dscanner.analysis.if_statements; import dscanner.analysis.redundant_parens; import dscanner.analysis.mismatched_args; import dscanner.analysis.label_var_same_name_check; @@ -72,7 +65,6 @@ import dscanner.analysis.properly_documented_public_functions; import dscanner.analysis.final_attribute; import dscanner.analysis.vcall_in_ctor; import dscanner.analysis.useless_initializer; -import dscanner.analysis.allman; import dscanner.analysis.always_curly; import dscanner.analysis.redundant_attributes; import dscanner.analysis.has_public_example; @@ -83,22 +75,25 @@ import dscanner.analysis.redundant_storage_class; import dscanner.analysis.unused_result; import dscanner.analysis.cyclomatic_complexity; import dscanner.analysis.body_on_disabled_funcs; - -import dsymbol.string_interning : internString; -import dsymbol.scope_; -import dsymbol.semantic; -import dsymbol.conversion; -import dsymbol.conversion.first; -import dsymbol.conversion.second; -import dsymbol.modulecache : ModuleCache; - -import dscanner.utils; import dscanner.reports : DScannerJsonReporter, SonarQubeGenericIssueDataReporter; +import dscanner.utils; + +import dmd.astbase : ASTBase; +import dmd.astcodegen; +import dmd.frontend; +import dmd.globals : global; +import dmd.parse : Parser; bool first = true; -private alias ASTAllocator = CAllocatorImpl!( - AllocatorList!(n => Region!Mallocator(1024 * 128), Mallocator)); +version (unittest) + enum ut = true; +else + enum ut = false; + +void doNothing(string, size_t, size_t, string, bool) +{ +} immutable string defaultErrorFormat = "{filepath}({line}:{column})[{type}]: {message}"; @@ -250,9 +245,7 @@ void writeJSON(Message message) writeln(" {"); writeln(` "key": "`, message.key, `",`); if (message.checkName !is null) - { writeln(` "name": "`, message.checkName, `",`); - } writeln(` "fileName": "`, message.fileName.replace("\\", "\\\\").replace(`"`, `\"`), `",`); writeln(` "line": `, message.startLine, `,`); writeln(` "column": `, message.startColumn, `,`); @@ -288,42 +281,32 @@ void writeJSON(Message message) write(" }"); } -bool syntaxCheck(string[] fileNames, string errorFormat, ref StringCache stringCache, ref ModuleCache moduleCache) +bool syntaxCheck(string[] fileNames, string errorFormat) { StaticAnalysisConfig config = defaultStaticAnalysisConfig(); - return analyze(fileNames, config, errorFormat, stringCache, moduleCache, false); + return analyze(fileNames, config, errorFormat); } -void generateReport(string[] fileNames, const StaticAnalysisConfig config, - ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "") +void generateReport(string[] fileNames, const StaticAnalysisConfig config, string reportFile = "") { auto reporter = new DScannerJsonReporter(); - - auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){ - // TODO: proper index and column ranges - reporter.addMessage( - Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"), - isError); - }; - first = true; - StatsCollector stats = new StatsCollector(BaseAnalyzerArguments.init); + auto statsCollector = new StatsCollector!ASTCodegen(); ulong lineOfCodeCount; + foreach (fileName; fileNames) { auto code = readFile(fileName); // Skip files that could not be read and continue with the rest if (code.length == 0) continue; - RollbackAllocator r; - const(Token)[] tokens; - const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, &lineOfCodeCount, null, null); - stats.visit(m); - MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true); + auto dmdModule = parseDmdModule(fileName, cast(string) code); + dmdModule.accept(statsCollector); + MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config); reporter.addMessageSet(messageSet); } - string reportFileContent = reporter.getContent(stats, lineOfCodeCount); + string reportFileContent = reporter.getContent(statsCollector, lineOfCodeCount); if (reportFile == "") { writeln(reportFileContent); @@ -336,27 +319,18 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config, } void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAnalysisConfig config, - ref StringCache cache, ref ModuleCache moduleCache, string reportFile = "") + string reportFile = "") { auto reporter = new SonarQubeGenericIssueDataReporter(); - auto writeMessages = delegate void(string fileName, size_t line, size_t column, string message, bool isError){ - // TODO: proper index and column ranges - reporter.addMessage( - Message(Message.Diagnostic.from(fileName, [0, 0], line, [column, column], message), "dscanner.syntax"), - isError); - }; - foreach (fileName; fileNames) { auto code = readFile(fileName); // Skip files that could not be read and continue with the rest if (code.length == 0) continue; - RollbackAllocator r; - const(Token)[] tokens; - const Module m = parseModule(fileName, code, &r, cache, tokens, writeMessages, null, null, null); - MessageSet messageSet = analyze(fileName, m, config, moduleCache, tokens, true); + auto dmdModule = parseDmdModule(fileName, cast(string) code); + MessageSet messageSet = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config); reporter.addMessageSet(messageSet); } @@ -377,32 +351,49 @@ void generateSonarQubeGenericIssueDataReport(string[] fileNames, const StaticAna * * Returns: true if there were errors or if there were warnings and `staticAnalyze` was true. */ -bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, - ref StringCache cache, ref ModuleCache moduleCache, bool staticAnalyze = true) +bool analyze(string[] fileNames, const StaticAnalysisConfig config, string errorFormat) { + import std.file : exists, remove; + bool hasErrors; foreach (fileName; fileNames) { - auto code = readFile(fileName); + bool isStdin; + ubyte[] code; + + if (fileName == "stdin") + { + code = readStdin(); + fileName = "stdin.d"; + File f = File(fileName, "w"); + f.rawWrite(code); + f.close(); + isStdin = true; + } + else + { + code = readFile(fileName); + } // Skip files that could not be read and continue with the rest if (code.length == 0) continue; - RollbackAllocator r; - uint errorCount; - uint warningCount; - const(Token)[] tokens; - const Module m = parseModule(fileName, code, &r, errorFormat, cache, false, tokens, - null, &errorCount, &warningCount); - assert(m); - if (errorCount > 0 || (staticAnalyze && warningCount > 0)) + + auto dmdModule = parseDmdModule(fileName, cast(string) code); + if (global.errors > 0 || global.warnings > 0) hasErrors = true; - MessageSet results = analyze(fileName, m, config, moduleCache, tokens, staticAnalyze); + MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config); + if (results is null) continue; + + hasErrors = !results.empty; foreach (result; results[]) - { - hasErrors = true; messageFunctionFormat(errorFormat, result, false, code); + + if (isStdin) + { + assert(exists(fileName)); + remove(fileName); } } return hasErrors; @@ -413,9 +404,7 @@ bool analyze(string[] fileNames, const StaticAnalysisConfig config, string error * * Returns: true if there were parse errors. */ -bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, - ref StringCache cache, ref ModuleCache moduleCache, bool autoApplySingle, - const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid) +bool autofix(string[] fileNames, const StaticAnalysisConfig config, string errorFormat, bool autoApplySingle) { import std.format : format; @@ -426,16 +415,11 @@ bool autofix(string[] fileNames, const StaticAnalysisConfig config, string error // Skip files that could not be read and continue with the rest if (code.length == 0) continue; - RollbackAllocator r; - uint errorCount; - uint warningCount; - const(Token)[] tokens; - const Module m = parseModule(fileName, code, &r, errorFormat, cache, false, tokens, - null, &errorCount, &warningCount); - assert(m); - if (errorCount > 0) + auto dmdModule = parseDmdModule(fileName, cast(string) code); + if (global.errors > 0) hasErrors = true; - MessageSet results = analyze(fileName, m, config, moduleCache, tokens, true, true, overrideFormattingConfig); + + MessageSet results = analyzeDmd(fileName, dmdModule, getModuleName(dmdModule.md), config); if (results is null) continue; @@ -482,90 +466,6 @@ bool autofix(string[] fileNames, const StaticAnalysisConfig config, string error return hasErrors; } -void listAutofixes( - StaticAnalysisConfig config, - string resolveMessage, - bool usingStdin, - string fileName, - StringCache* cache, - ref ModuleCache moduleCache -) -{ - import dparse.parser : parseModule; - import dscanner.analysis.base : Message; - import std.format : format; - import std.json : JSONValue; - - union RequestedLocation - { - struct - { - uint line, column; - } - ulong bytes; - } - - RequestedLocation req; - bool isBytes = resolveMessage[0] == 'b'; - if (isBytes) - req.bytes = resolveMessage[1 .. $].to!ulong; - else - { - auto parts = resolveMessage.findSplit(":"); - req.line = parts[0].to!uint; - req.column = parts[2].to!uint; - } - - bool matchesCursor(Message m) - { - return isBytes - ? req.bytes >= m.startIndex && req.bytes <= m.endIndex - : req.line >= m.startLine && req.line <= m.endLine - && (req.line > m.startLine || req.column >= m.startColumn) - && (req.line < m.endLine || req.column <= m.endColumn); - } - - RollbackAllocator rba; - LexerConfig lexerConfig; - lexerConfig.fileName = fileName; - lexerConfig.stringBehavior = StringBehavior.source; - auto tokens = getTokensForParser(usingStdin ? readStdin() - : readFile(fileName), lexerConfig, cache); - auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing)); - - auto messages = analyze(fileName, mod, config, moduleCache, tokens); - - with (stdout.lockingTextWriter) - { - put("["); - foreach (message; messages[].filter!matchesCursor) - { - resolveAutoFixes(message, fileName, moduleCache, tokens, mod, config); - - foreach (i, autofix; message.autofixes) - { - put(i == 0 ? "\n" : ",\n"); - put("\t{\n"); - put(format!"\t\t\"name\": %s,\n"(JSONValue(autofix.name))); - put("\t\t\"replacements\": ["); - foreach (j, replacement; autofix.expectReplacements) - { - put(j == 0 ? "\n" : ",\n"); - put(format!"\t\t\t{\"range\": [%d, %d], \"newText\": %s}"( - replacement.range[0], - replacement.range[1], - JSONValue(replacement.newText))); - } - put("\n"); - put("\t\t]\n"); - put("\t}"); - } - } - put("\n]"); - } - stdout.flush(); -} - private struct UserSelect { import std.string : strip; @@ -659,589 +559,6 @@ const(Module) parseModule(string fileName, ubyte[] code, RollbackAllocator* p, linesOfCode, errorCount, warningCount); } -/** -Checks whether a module is part of a user-specified include/exclude list. -The user can specify a comma-separated list of filters, everyone needs to start with -either a '+' (inclusion) or '-' (exclusion). -If no includes are specified, all modules are included. -*/ -bool shouldRun(check : BaseAnalyzer)(string moduleName, const ref StaticAnalysisConfig config) -{ - enum string a = check.name; - - if (mixin("config." ~ a) == Check.disabled) - return false; - - // By default, run the check - if (!moduleName.length) - return true; - - auto filters = mixin("config.filters." ~ a); - - // Check if there are filters are defined - // filters starting with a comma are invalid - if (filters.length == 0 || filters[0].length == 0) - return true; - - auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]); - auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]); - - // exclusion has preference over inclusion - if (!excluders.empty && excluders.any!(s => moduleName.canFind(s))) - return false; - - if (!includers.empty) - return includers.any!(s => moduleName.canFind(s)); - - // by default: include all modules - return true; -} - -/// -unittest -{ - bool test(string moduleName, string filters) - { - StaticAnalysisConfig config; - // it doesn't matter which check we test here - config.asm_style_check = Check.enabled; - // this is done automatically by inifiled - config.filters.asm_style_check = filters.split(","); - return shouldRun!AsmStyleCheck(moduleName, config); - } - - // test inclusion - assert(test("std.foo", "+std.")); - // partial matches are ok - assert(test("std.foo", "+bar,+foo")); - // full as well - assert(test("std.foo", "+bar,+std.foo,+foo")); - // mismatch - assert(!test("std.foo", "+bar,+banana")); - - // test exclusion - assert(!test("std.foo", "-std.")); - assert(!test("std.foo", "-bar,-std.foo")); - assert(!test("std.foo", "-bar,-foo")); - // mismatch - assert(test("std.foo", "-bar,-banana")); - - // test combination (exclusion has precedence) - assert(!test("std.foo", "+foo,-foo")); - assert(test("std.foo", "+foo,-bar")); - assert(test("std.bar.foo", "-barr,+bar")); -} - -private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName, - const(Token)[] tokens, const Module m, - const StaticAnalysisConfig analysisConfig, const Scope* moduleScope) -{ - version (unittest) - enum ut = true; - else - enum ut = false; - - BaseAnalyzer[] checks; - - string moduleName; - if (m !is null && m.moduleDeclaration !is null && - m.moduleDeclaration.moduleName !is null && - m.moduleDeclaration.moduleName.identifiers !is null) - moduleName = m.moduleDeclaration.moduleName.identifiers.map!(e => e.text).join("."); - - BaseAnalyzerArguments args = BaseAnalyzerArguments( - fileName, - tokens, - moduleScope - ); - - if (moduleName.shouldRun!AsmStyleCheck(analysisConfig)) - checks ~= new AsmStyleCheck(args.setSkipTests( - analysisConfig.asm_style_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!BackwardsRangeCheck(analysisConfig)) - checks ~= new BackwardsRangeCheck(args.setSkipTests( - analysisConfig.backwards_range_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!BuiltinPropertyNameCheck(analysisConfig)) - checks ~= new BuiltinPropertyNameCheck(args.setSkipTests( - analysisConfig.builtin_property_names_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!CommaExpressionCheck(analysisConfig)) - checks ~= new CommaExpressionCheck(args.setSkipTests( - analysisConfig.comma_expression_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!ConstructorCheck(analysisConfig)) - checks ~= new ConstructorCheck(args.setSkipTests( - analysisConfig.constructor_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UnmodifiedFinder(analysisConfig)) - checks ~= new UnmodifiedFinder(args.setSkipTests( - analysisConfig.could_be_immutable_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!DeleteCheck(analysisConfig)) - checks ~= new DeleteCheck(args.setSkipTests( - analysisConfig.delete_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!DuplicateAttributeCheck(analysisConfig)) - checks ~= new DuplicateAttributeCheck(args.setSkipTests( - analysisConfig.duplicate_attribute == Check.skipTests && !ut)); - - if (moduleName.shouldRun!EnumArrayLiteralCheck(analysisConfig)) - checks ~= new EnumArrayLiteralCheck(args.setSkipTests( - analysisConfig.enum_array_literal_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!PokemonExceptionCheck(analysisConfig)) - checks ~= new PokemonExceptionCheck(args.setSkipTests( - analysisConfig.exception_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!FloatOperatorCheck(analysisConfig)) - checks ~= new FloatOperatorCheck(args.setSkipTests( - analysisConfig.float_operator_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!FunctionAttributeCheck(analysisConfig)) - checks ~= new FunctionAttributeCheck(args.setSkipTests( - analysisConfig.function_attribute_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!IfElseSameCheck(analysisConfig)) - checks ~= new IfElseSameCheck(args.setSkipTests( - analysisConfig.if_else_same_check == Check.skipTests&& !ut)); - - if (moduleName.shouldRun!LabelVarNameCheck(analysisConfig)) - checks ~= new LabelVarNameCheck(args.setSkipTests( - analysisConfig.label_var_same_name_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!LengthSubtractionCheck(analysisConfig)) - checks ~= new LengthSubtractionCheck(args.setSkipTests( - analysisConfig.length_subtraction_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!LocalImportCheck(analysisConfig)) - checks ~= new LocalImportCheck(args.setSkipTests( - analysisConfig.local_import_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!LogicPrecedenceCheck(analysisConfig)) - checks ~= new LogicPrecedenceCheck(args.setSkipTests( - analysisConfig.logical_precedence_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!MismatchedArgumentCheck(analysisConfig)) - checks ~= new MismatchedArgumentCheck(args.setSkipTests( - analysisConfig.mismatched_args_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!NumberStyleCheck(analysisConfig)) - checks ~= new NumberStyleCheck(args.setSkipTests( - analysisConfig.number_style_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!ObjectConstCheck(analysisConfig)) - checks ~= new ObjectConstCheck(args.setSkipTests( - analysisConfig.object_const_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!OpEqualsWithoutToHashCheck(analysisConfig)) - checks ~= new OpEqualsWithoutToHashCheck(args.setSkipTests( - analysisConfig.opequals_tohash_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!RedundantParenCheck(analysisConfig)) - checks ~= new RedundantParenCheck(args.setSkipTests( - analysisConfig.redundant_parens_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!StyleChecker(analysisConfig)) - checks ~= new StyleChecker(args.setSkipTests( - analysisConfig.style_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UndocumentedDeclarationCheck(analysisConfig)) - checks ~= new UndocumentedDeclarationCheck(args.setSkipTests( - analysisConfig.undocumented_declaration_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UnusedLabelCheck(analysisConfig)) - checks ~= new UnusedLabelCheck(args.setSkipTests( - analysisConfig.unused_label_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UnusedVariableCheck(analysisConfig)) - checks ~= new UnusedVariableCheck(args.setSkipTests( - analysisConfig.unused_variable_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UnusedParameterCheck(analysisConfig)) - checks ~= new UnusedParameterCheck(args.setSkipTests( - analysisConfig.unused_parameter_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!LineLengthCheck(analysisConfig)) - checks ~= new LineLengthCheck(args.setSkipTests( - analysisConfig.long_line_check == Check.skipTests && !ut), - analysisConfig.max_line_length); - - if (moduleName.shouldRun!AutoRefAssignmentCheck(analysisConfig)) - checks ~= new AutoRefAssignmentCheck(args.setSkipTests( - analysisConfig.auto_ref_assignment_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!IncorrectInfiniteRangeCheck(analysisConfig)) - checks ~= new IncorrectInfiniteRangeCheck(args.setSkipTests( - analysisConfig.incorrect_infinite_range_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UselessAssertCheck(analysisConfig)) - checks ~= new UselessAssertCheck(args.setSkipTests( - analysisConfig.useless_assert_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!AliasSyntaxCheck(analysisConfig)) - checks ~= new AliasSyntaxCheck(args.setSkipTests( - analysisConfig.alias_syntax_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!StaticIfElse(analysisConfig)) - checks ~= new StaticIfElse(args.setSkipTests( - analysisConfig.static_if_else_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!LambdaReturnCheck(analysisConfig)) - checks ~= new LambdaReturnCheck(args.setSkipTests( - analysisConfig.lambda_return_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!AutoFunctionChecker(analysisConfig)) - checks ~= new AutoFunctionChecker(args.setSkipTests( - analysisConfig.auto_function_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!ImportSortednessCheck(analysisConfig)) - checks ~= new ImportSortednessCheck(args.setSkipTests( - analysisConfig.imports_sortedness == Check.skipTests && !ut)); - - if (moduleName.shouldRun!ExplicitlyAnnotatedUnittestCheck(analysisConfig)) - checks ~= new ExplicitlyAnnotatedUnittestCheck(args.setSkipTests( - analysisConfig.explicitly_annotated_unittests == Check.skipTests && !ut)); - - if (moduleName.shouldRun!ProperlyDocumentedPublicFunctions(analysisConfig)) - checks ~= new ProperlyDocumentedPublicFunctions(args.setSkipTests( - analysisConfig.properly_documented_public_functions == Check.skipTests && !ut)); - - if (moduleName.shouldRun!FinalAttributeChecker(analysisConfig)) - checks ~= new FinalAttributeChecker(args.setSkipTests( - analysisConfig.final_attribute_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!VcallCtorChecker(analysisConfig)) - checks ~= new VcallCtorChecker(args.setSkipTests( - analysisConfig.vcall_in_ctor == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UselessInitializerChecker(analysisConfig)) - checks ~= new UselessInitializerChecker(args.setSkipTests( - analysisConfig.useless_initializer == Check.skipTests && !ut)); - - if (moduleName.shouldRun!AllManCheck(analysisConfig)) - checks ~= new AllManCheck(args.setSkipTests( - analysisConfig.allman_braces_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!AlwaysCurlyCheck(analysisConfig)) - checks ~= new AlwaysCurlyCheck(args.setSkipTests( - analysisConfig.always_curly_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!RedundantAttributesCheck(analysisConfig)) - checks ~= new RedundantAttributesCheck(args.setSkipTests( - analysisConfig.redundant_attributes_check == Check.skipTests && !ut)); - - if (moduleName.shouldRun!HasPublicExampleCheck(analysisConfig)) - checks ~= new HasPublicExampleCheck(args.setSkipTests( - analysisConfig.has_public_example == Check.skipTests && !ut)); - - if (moduleName.shouldRun!AssertWithoutMessageCheck(analysisConfig)) - checks ~= new AssertWithoutMessageCheck(args.setSkipTests( - analysisConfig.assert_without_msg == Check.skipTests && !ut)); - - if (moduleName.shouldRun!IfConstraintsIndentCheck(analysisConfig)) - checks ~= new IfConstraintsIndentCheck(args.setSkipTests( - analysisConfig.if_constraints_indent == Check.skipTests && !ut)); - - if (moduleName.shouldRun!TrustTooMuchCheck(analysisConfig)) - checks ~= new TrustTooMuchCheck(args.setSkipTests( - analysisConfig.trust_too_much == Check.skipTests && !ut)); - - if (moduleName.shouldRun!RedundantStorageClassCheck(analysisConfig)) - checks ~= new RedundantStorageClassCheck(args.setSkipTests( - analysisConfig.redundant_storage_classes == Check.skipTests && !ut)); - - if (moduleName.shouldRun!UnusedResultChecker(analysisConfig)) - checks ~= new UnusedResultChecker(args.setSkipTests( - analysisConfig.unused_result == Check.skipTests && !ut)); - - if (moduleName.shouldRun!CyclomaticComplexityCheck(analysisConfig)) - checks ~= new CyclomaticComplexityCheck(args.setSkipTests( - analysisConfig.cyclomatic_complexity == Check.skipTests && !ut), - analysisConfig.max_cyclomatic_complexity.to!int); - - if (moduleName.shouldRun!BodyOnDisabledFuncsCheck(analysisConfig)) - checks ~= new BodyOnDisabledFuncsCheck(args.setSkipTests( - analysisConfig.body_on_disabled_func_check == Check.skipTests && !ut)); - - version (none) - if (moduleName.shouldRun!IfStatementCheck(analysisConfig)) - checks ~= new IfStatementCheck(args.setSkipTests( - analysisConfig.redundant_if_check == Check.skipTests && !ut)); - - return checks; -} - -MessageSet analyze(string fileName, const Module m, const StaticAnalysisConfig analysisConfig, - ref ModuleCache moduleCache, const(Token)[] tokens, bool staticAnalyze = true, - bool resolveAutoFixes = false, - const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid) -{ - import dsymbol.symbol : DSymbol; - - if (!staticAnalyze) - return null; - - const(AutoFixFormatting) formattingConfig = - (resolveAutoFixes && overrideFormattingConfig is AutoFixFormatting.invalid) - ? analysisConfig.getAutoFixFormattingConfig() - : overrideFormattingConfig; - - scope first = new FirstPass(m, internString(fileName), &moduleCache, null); - first.run(); - - secondPass(first.rootSymbol, first.moduleScope, moduleCache); - auto moduleScope = first.moduleScope; - scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol); - scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol); - scope(exit) typeid(Scope).destroy(first.moduleScope); - - GC.disable; - scope (exit) - GC.enable; - - MessageSet set = new MessageSet; - foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope)) - { - check.visit(m); - foreach (message; check.messages) - { - if (resolveAutoFixes) - foreach (ref autofix; message.autofixes) - autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig); - set.insert(message); - } - } - - return set; -} - -private void resolveAutoFixFromCheck( - ref AutoFix autofix, - BaseAnalyzer check, - const Module m, - scope const(Token)[] tokens, - const AutoFixFormatting formattingConfig -) -{ - import std.sumtype : match; - - autofix.replacements.match!( - (AutoFix.ResolveContext context) { - autofix.replacements = check.resolveAutoFix(m, tokens, context, formattingConfig); - }, - (_) {} - ); -} - -void resolveAutoFixes(ref Message message, string fileName, - ref ModuleCache moduleCache, - scope const(Token)[] tokens, const Module m, - const StaticAnalysisConfig analysisConfig, - const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid) -{ - resolveAutoFixes(message.checkName, message.autofixes, fileName, moduleCache, - tokens, m, analysisConfig, overrideFormattingConfig); -} - -AutoFix.CodeReplacement[] resolveAutoFix(string messageCheckName, AutoFix.ResolveContext context, - string fileName, - ref ModuleCache moduleCache, - scope const(Token)[] tokens, const Module m, - const StaticAnalysisConfig analysisConfig, - const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid) -{ - AutoFix temp; - temp.replacements = context; - resolveAutoFixes(messageCheckName, (&temp)[0 .. 1], fileName, moduleCache, - tokens, m, analysisConfig, overrideFormattingConfig); - return temp.expectReplacements("resolving didn't work?!"); -} - -void resolveAutoFixes(string messageCheckName, AutoFix[] autofixes, string fileName, - ref ModuleCache moduleCache, - scope const(Token)[] tokens, const Module m, - const StaticAnalysisConfig analysisConfig, - const AutoFixFormatting overrideFormattingConfig = AutoFixFormatting.invalid) -{ - import dsymbol.symbol : DSymbol; - - const(AutoFixFormatting) formattingConfig = - overrideFormattingConfig is AutoFixFormatting.invalid - ? analysisConfig.getAutoFixFormattingConfig() - : overrideFormattingConfig; - - scope first = new FirstPass(m, internString(fileName), &moduleCache, null); - first.run(); - - secondPass(first.rootSymbol, first.moduleScope, moduleCache); - auto moduleScope = first.moduleScope; - scope(exit) typeid(DSymbol).destroy(first.rootSymbol.acSymbol); - scope(exit) typeid(SemanticSymbol).destroy(first.rootSymbol); - scope(exit) typeid(Scope).destroy(first.moduleScope); - - GC.disable; - scope (exit) - GC.enable; - - foreach (BaseAnalyzer check; getAnalyzersForModuleAndConfig(fileName, tokens, m, analysisConfig, moduleScope)) - { - if (check.getName() == messageCheckName) - { - foreach (ref autofix; autofixes) - autofix.resolveAutoFixFromCheck(check, m, tokens, formattingConfig); - return; - } - } - - throw new Exception("Cannot find analyzer " ~ messageCheckName - ~ " to resolve autofix with."); -} - -void improveAutoFixWhitespace(scope const(char)[] code, AutoFix.CodeReplacement[] replacements) -{ - import std.ascii : isWhite; - import std.string : strip; - import std.utf : stride, strideBack; - - enum WS - { - none, tab, space, newline - } - - WS getWS(size_t i) - { - if (cast(ptrdiff_t) i < 0 || i >= code.length) - return WS.newline; - switch (code[i]) - { - case '\n': - case '\r': - return WS.newline; - case '\t': - return WS.tab; - case ' ': - return WS.space; - default: - return WS.none; - } - } - - foreach (ref replacement; replacements) - { - assert(replacement.range[0] >= 0 && replacement.range[0] < code.length - && replacement.range[1] >= 0 && replacement.range[1] < code.length - && replacement.range[0] <= replacement.range[1], "trying to autofix whitespace on code that doesn't match with what the replacements were generated for"); - - void growRight() - { - // this is basically: replacement.range[1]++; - if (code[replacement.range[1] .. $].startsWith("\r\n")) - replacement.range[1] += 2; - else if (replacement.range[1] < code.length) - replacement.range[1] += code.stride(replacement.range[1]); - } - - void growLeft() - { - // this is basically: replacement.range[0]--; - if (code[0 .. replacement.range[0]].endsWith("\r\n")) - replacement.range[0] -= 2; - else if (replacement.range[0] > 0) - replacement.range[0] -= code.strideBack(replacement.range[0]); - } - - if (replacement.newText.strip.length) - { - if (replacement.newText.startsWith(" ")) - { - // we insert with leading space, but there is a space/NL/SOF before - // remove to-be-inserted space - if (getWS(replacement.range[0] - 1)) - replacement.newText = replacement.newText[1 .. $]; - } - if (replacement.newText.startsWith("]", ")")) - { - // when inserting `)`, consume regular space before - if (getWS(replacement.range[0] - 1) == WS.space) - growLeft(); - } - if (replacement.newText.endsWith(" ")) - { - // we insert with trailing space, but there is a space/NL/EOF after, chomp off - if (getWS(replacement.range[1])) - replacement.newText = replacement.newText[0 .. $ - 1]; - } - if (replacement.newText.endsWith("[", "(")) - { - if (getWS(replacement.range[1])) - growRight(); - } - } - else if (!replacement.newText.length) - { - // after removing code and ending up with whitespace on both sides, - // collapse 2 whitespace into one - switch (getWS(replacement.range[1])) - { - case WS.newline: - switch (getWS(replacement.range[0] - 1)) - { - case WS.newline: - // after removal we have NL ~ NL or SOF ~ NL, - // remove right NL - growRight(); - break; - case WS.space: - case WS.tab: - // after removal we have space ~ NL, - // remove the space - growLeft(); - break; - default: - break; - } - break; - case WS.space: - case WS.tab: - // for NL ~ space, SOF ~ space, space ~ space, tab ~ space, - // for NL ~ tab, SOF ~ tab, space ~ tab, tab ~ tab - // remove right space/tab - if (getWS(replacement.range[0] - 1)) - growRight(); - break; - default: - break; - } - } - } -} - -unittest -{ - AutoFix.CodeReplacement r(int start, int end, string s) - { - return AutoFix.CodeReplacement([start, end], s); - } - - string test(string code, AutoFix.CodeReplacement[] replacements...) - { - replacements.sort!"a.range[0] < b.range[0]"; - improveAutoFixWhitespace(code, replacements); - foreach_reverse (r; replacements) - code = code[0 .. r.range[0]] ~ r.newText ~ code[r.range[1] .. $]; - return code; - } - - assert(test("import a;\nimport b;", r(0, 9, "")) == "import b;"); - assert(test("import a;\r\nimport b;", r(0, 9, "")) == "import b;"); - assert(test("import a;\nimport b;", r(8, 9, "")) == "import a\nimport b;"); - assert(test("import a;\nimport b;", r(7, 8, "")) == "import ;\nimport b;"); - assert(test("import a;\r\nimport b;", r(7, 8, "")) == "import ;\r\nimport b;"); - assert(test("a b c", r(2, 3, "")) == "a c"); -} - version (unittest) { shared static this() diff --git a/src/dscanner/analysis/rundmd.d b/src/dscanner/analysis/rundmd.d new file mode 100644 index 00000000..da23849b --- /dev/null +++ b/src/dscanner/analysis/rundmd.d @@ -0,0 +1,448 @@ +module dscanner.analysis.rundmd; + +import std.algorithm : any, canFind, filter, map; +import std.conv : to; + +import dmd.astcodegen; +import dmd.dmodule : Module; +import dmd.frontend; + +import dscanner.analysis.config : Check, StaticAnalysisConfig; +import dscanner.analysis.base : BaseAnalyzerDmd, MessageSet; + +import dscanner.analysis.alias_syntax_check : AliasSyntaxCheck; +import dscanner.analysis.always_curly : AlwaysCurlyCheck; +import dscanner.analysis.asm_style : AsmStyleCheck; +import dscanner.analysis.assert_without_msg : AssertWithoutMessageCheck; +import dscanner.analysis.auto_function : AutoFunctionChecker; +import dscanner.analysis.auto_ref_assignment : AutoRefAssignmentCheck; +import dscanner.analysis.body_on_disabled_funcs : BodyOnDisabledFuncsCheck; +import dscanner.analysis.builtin_property_names : BuiltinPropertyNameCheck; +import dscanner.analysis.constructors : ConstructorCheck; +import dscanner.analysis.cyclomatic_complexity : CyclomaticComplexityCheck; +import dscanner.analysis.del : DeleteCheck; +import dscanner.analysis.enumarrayliteral : EnumArrayVisitor; +import dscanner.analysis.explicitly_annotated_unittests : ExplicitlyAnnotatedUnittestCheck; +import dscanner.analysis.final_attribute : FinalAttributeChecker; +import dscanner.analysis.function_attributes : FunctionAttributeCheck; +import dscanner.analysis.has_public_example : HasPublicExampleCheck; +import dscanner.analysis.if_constraints_indent : IfConstraintsIndentCheck; +import dscanner.analysis.ifelsesame : IfElseSameCheck; +import dscanner.analysis.imports_sortedness : ImportSortednessCheck; +import dscanner.analysis.incorrect_infinite_range : IncorrectInfiniteRangeCheck; +import dscanner.analysis.label_var_same_name_check : LabelVarNameCheck; +import dscanner.analysis.lambda_return_check : LambdaReturnCheck; +import dscanner.analysis.length_subtraction : LengthSubtractionCheck; +import dscanner.analysis.line_length : LineLengthCheck; +import dscanner.analysis.local_imports : LocalImportCheck; +import dscanner.analysis.logic_precedence : LogicPrecedenceCheck; +import dscanner.analysis.mismatched_args : MismatchedArgumentCheck; +import dscanner.analysis.numbers : NumberStyleCheck; +import dscanner.analysis.objectconst : ObjectConstCheck; +import dscanner.analysis.opequals_without_tohash : OpEqualsWithoutToHashCheck; +import dscanner.analysis.pokemon : PokemonExceptionCheck; +import dscanner.analysis.properly_documented_public_functions : ProperlyDocumentedPublicFunctions; +import dscanner.analysis.range : BackwardsRangeCheck; +import dscanner.analysis.redundant_attributes : RedundantAttributesCheck; +import dscanner.analysis.redundant_parens : RedundantParenCheck; +import dscanner.analysis.redundant_storage_class : RedundantStorageClassCheck; +import dscanner.analysis.static_if_else : StaticIfElse; +import dscanner.analysis.style : StyleChecker; +import dscanner.analysis.trust_too_much : TrustTooMuchCheck; +import dscanner.analysis.undocumented : UndocumentedDeclarationCheck; +import dscanner.analysis.unmodified : UnmodifiedFinder; +import dscanner.analysis.unused_label : UnusedLabelCheck; +import dscanner.analysis.unused_parameter : UnusedParameterCheck; +import dscanner.analysis.unused_result : UnusedResultChecker; +import dscanner.analysis.unused_variable : UnusedVariableCheck; +import dscanner.analysis.useless_assert : UselessAssertCheck; +import dscanner.analysis.useless_initializer : UselessInitializerChecker; +import dscanner.analysis.vcall_in_ctor : VcallCtorChecker; +import dscanner.analysis.allman : AllManCheck; + +version (unittest) + enum ut = true; +else + enum ut = false; + +Module parseDmdModule(string fileName, string sourceCode) +{ + setupDmd(); + + auto code = sourceCode; + if (code[$ - 1] != '\0') + code ~= '\0'; + + auto dmdModule = dmd.frontend.parseModule(cast(const(char)[]) fileName, cast(const (char)[]) code); + return dmdModule.module_; +} + +private void setupDmd() +{ + import std.path : dirName; + import dmd.globals : global, ImportPathInfo; + + auto dmdParentDir = dirName(dirName(dirName(dirName(__FILE_FULL_PATH__)))); + auto dmdDirPath = dmdParentDir ~ "/dmd" ~ "\0"; + auto druntimeDirPath = dmdParentDir ~ "/dmd/druntime/src" ~ "\0"; + global.params.useUnitTests = true; + global.path.push(ImportPathInfo(dmdDirPath.ptr)); + global.path.push(ImportPathInfo(druntimeDirPath.ptr)); + global.errors = 0; + initDMD(); +} + +MessageSet analyzeDmd(string fileName, ASTCodegen.Module m, const char[] moduleName, const StaticAnalysisConfig config) +{ + MessageSet set = new MessageSet; + auto visitors = getDmdAnalyzersForModuleAndConfig(fileName, config, moduleName); + + foreach (visitor; visitors) + { + m.accept(visitor); + + foreach (message; visitor.messages) + set.insert(message); + } + + return set; +} + +BaseAnalyzerDmd[] getDmdAnalyzersForModuleAndConfig(string fileName, const StaticAnalysisConfig config, + const char[] moduleName) +{ + BaseAnalyzerDmd[] visitors; + + if (moduleName.shouldRunDmd!(ObjectConstCheck!ASTCodegen)(config)) + visitors ~= new ObjectConstCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(EnumArrayVisitor!ASTCodegen)(config)) + visitors ~= new EnumArrayVisitor!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(DeleteCheck!ASTCodegen)(config)) + visitors ~= new DeleteCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(FinalAttributeChecker!ASTCodegen)(config)) + visitors ~= new FinalAttributeChecker!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(ImportSortednessCheck!ASTCodegen)(config)) + visitors ~= new ImportSortednessCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(IncorrectInfiniteRangeCheck!ASTCodegen)(config)) + visitors ~= new IncorrectInfiniteRangeCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(RedundantAttributesCheck!ASTCodegen)(config)) + visitors ~= new RedundantAttributesCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(LengthSubtractionCheck!ASTCodegen)(config)) + visitors ~= new LengthSubtractionCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(AliasSyntaxCheck!ASTCodegen)(config)) + visitors ~= new AliasSyntaxCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(ExplicitlyAnnotatedUnittestCheck!ASTCodegen)(config)) + visitors ~= new ExplicitlyAnnotatedUnittestCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(ConstructorCheck!ASTCodegen)(config)) + visitors ~= new ConstructorCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(AssertWithoutMessageCheck!ASTCodegen)(config)) + visitors ~= new AssertWithoutMessageCheck!ASTCodegen( + fileName, + config.assert_without_msg == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(LocalImportCheck!ASTCodegen)(config)) + visitors ~= new LocalImportCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(OpEqualsWithoutToHashCheck!ASTCodegen)(config)) + visitors ~= new OpEqualsWithoutToHashCheck!ASTCodegen( + fileName, + config.opequals_tohash_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(TrustTooMuchCheck!ASTCodegen)(config)) + visitors ~= new TrustTooMuchCheck!ASTCodegen( + fileName, + config.trust_too_much == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(AutoRefAssignmentCheck!ASTCodegen)(config)) + visitors ~= new AutoRefAssignmentCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(LogicPrecedenceCheck!ASTCodegen)(config)) + visitors ~= new LogicPrecedenceCheck!ASTCodegen( + fileName, + config.logical_precedence_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UnusedLabelCheck!ASTCodegen)(config)) + visitors ~= new UnusedLabelCheck!ASTCodegen( + fileName, + config.unused_label_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(BuiltinPropertyNameCheck!ASTCodegen)(config)) + visitors ~= new BuiltinPropertyNameCheck!ASTCodegen(fileName); + + if (moduleName.shouldRunDmd!(PokemonExceptionCheck!ASTCodegen)(config)) + visitors ~= new PokemonExceptionCheck!ASTCodegen( + fileName, + config.exception_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(BackwardsRangeCheck!ASTCodegen)(config)) + visitors ~= new BackwardsRangeCheck!ASTCodegen( + fileName, + config.backwards_range_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(ProperlyDocumentedPublicFunctions!ASTCodegen)(config)) + visitors ~= new ProperlyDocumentedPublicFunctions!ASTCodegen( + fileName, + config.properly_documented_public_functions == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(RedundantParenCheck!ASTCodegen)(config)) + visitors ~= new RedundantParenCheck!ASTCodegen( + fileName, + config.redundant_parens_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(StaticIfElse!ASTCodegen)(config)) + visitors ~= new StaticIfElse!ASTCodegen( + fileName, + config.static_if_else_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UselessAssertCheck!ASTCodegen)(config)) + visitors ~= new UselessAssertCheck!ASTCodegen( + fileName, + config.useless_assert_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config)) + visitors ~= new AsmStyleCheck!ASTCodegen( + fileName, + config.asm_style_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(RedundantStorageClassCheck!ASTCodegen)(config)) + visitors ~= new RedundantStorageClassCheck!ASTCodegen( + fileName, + config.redundant_storage_classes == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(NumberStyleCheck!ASTCodegen)(config)) + visitors ~= new NumberStyleCheck!ASTCodegen( + fileName, + config.number_style_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(IfElseSameCheck!ASTCodegen)(config)) + visitors ~= new IfElseSameCheck!ASTCodegen( + fileName, + config.if_else_same_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(CyclomaticComplexityCheck!ASTCodegen)(config)) + visitors ~= new CyclomaticComplexityCheck!ASTCodegen( + fileName, + config.cyclomatic_complexity == Check.skipTests && !ut, + config.max_cyclomatic_complexity.to!int + ); + + if (moduleName.shouldRunDmd!(LabelVarNameCheck!ASTCodegen)(config)) + visitors ~= new LabelVarNameCheck!ASTCodegen( + fileName, + config.label_var_same_name_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(LambdaReturnCheck!ASTCodegen)(config)) + visitors ~= new LambdaReturnCheck!ASTCodegen( + fileName, + config.lambda_return_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(AlwaysCurlyCheck!ASTCodegen)(config)) + visitors ~= new AlwaysCurlyCheck!ASTCodegen( + fileName, + config.always_curly_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(StyleChecker!ASTCodegen)(config)) + visitors ~= new StyleChecker!ASTCodegen( + fileName, + config.style_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(AutoFunctionChecker!ASTCodegen)(config)) + visitors ~= new AutoFunctionChecker!ASTCodegen( + fileName, + config.auto_function_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UnusedParameterCheck!ASTCodegen)(config)) + visitors ~= new UnusedParameterCheck!ASTCodegen( + fileName, + config.unused_parameter_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UnusedVariableCheck!ASTCodegen)(config)) + visitors ~= new UnusedVariableCheck!ASTCodegen( + fileName, + config.unused_variable_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UnmodifiedFinder!ASTCodegen)(config)) + visitors ~= new UnmodifiedFinder!ASTCodegen( + fileName, + config.could_be_immutable_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(BodyOnDisabledFuncsCheck!ASTCodegen)(config)) + visitors ~= new BodyOnDisabledFuncsCheck!ASTCodegen( + fileName, + config.body_on_disabled_func_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UselessInitializerChecker!ASTCodegen)(config)) + visitors ~= new UselessInitializerChecker!ASTCodegen( + fileName, + config.useless_initializer == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(HasPublicExampleCheck!ASTCodegen)(config)) + visitors ~= new HasPublicExampleCheck!ASTCodegen( + fileName, + config.has_public_example == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!LineLengthCheck(config)) + visitors ~= new LineLengthCheck( + fileName, + config.long_line_check == Check.skipTests && !ut, + config.max_line_length + ); + + if (moduleName.shouldRunDmd!(UnusedResultChecker!ASTCodegen)(config)) + visitors ~= new UnusedResultChecker!ASTCodegen( + fileName, + config.unused_result == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(VcallCtorChecker!ASTCodegen)(config)) + visitors ~= new VcallCtorChecker!ASTCodegen( + fileName, + config.vcall_in_ctor == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!AllManCheck(config)) + visitors ~= new AllManCheck( + fileName, + config.allman_braces_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(MismatchedArgumentCheck!ASTCodegen)(config)) + visitors ~= new MismatchedArgumentCheck!ASTCodegen( + fileName, + config.mismatched_args_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(IfConstraintsIndentCheck!ASTCodegen)(config)) + visitors ~= new IfConstraintsIndentCheck!ASTCodegen( + fileName, + config.if_constraints_indent == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(UndocumentedDeclarationCheck!ASTCodegen)(config)) + visitors ~= new UndocumentedDeclarationCheck!ASTCodegen( + fileName, + config.undocumented_declaration_check == Check.skipTests && !ut + ); + + if (moduleName.shouldRunDmd!(FunctionAttributeCheck!ASTCodegen)(config)) + visitors ~= new FunctionAttributeCheck!ASTCodegen( + fileName, + config.function_attribute_check == Check.skipTests && !ut + ); + + return visitors; +} + +/** + * Checks whether a module is part of a user-specified include/exclude list. + * + * The user can specify a comma-separated list of filters, everyone needs to start with + * either a '+' (inclusion) or '-' (exclusion). + * + * If no includes are specified, all modules are included. +*/ +private bool shouldRunDmd(check : BaseAnalyzerDmd)(const char[] moduleName, const ref StaticAnalysisConfig config) +{ + enum string a = check.name; + + if (mixin("config." ~ a) == Check.disabled) + return false; + + // By default, run the check + if (!moduleName.length) + return true; + + auto filters = mixin("config.filters." ~ a); + + // Check if there are filters are defined + // filters starting with a comma are invalid + if (filters.length == 0 || filters[0].length == 0) + return true; + + auto includers = filters.filter!(f => f[0] == '+').map!(f => f[1..$]); + auto excluders = filters.filter!(f => f[0] == '-').map!(f => f[1..$]); + + // exclusion has preference over inclusion + if (!excluders.empty && excluders.any!(s => moduleName.canFind(s))) + return false; + + if (!includers.empty) + return includers.any!(s => moduleName.canFind(s)); + + // by default: include all modules + return true; +} + +/// +unittest +{ + bool test(string moduleName, string filters) + { + import std.array : split; + + StaticAnalysisConfig config; + // it doesn't matter which check we test here + config.asm_style_check = Check.enabled; + // this is done automatically by inifiled + config.filters.asm_style_check = filters.split(","); + return moduleName.shouldRunDmd!(AsmStyleCheck!ASTCodegen)(config); + } + + // test inclusion + assert(test("std.foo", "+std.")); + // partial matches are ok + assert(test("std.foo", "+bar,+foo")); + // full as well + assert(test("std.foo", "+bar,+std.foo,+foo")); + // mismatch + assert(!test("std.foo", "+bar,+banana")); + + // test exclusion + assert(!test("std.foo", "-std.")); + assert(!test("std.foo", "-bar,-std.foo")); + assert(!test("std.foo", "-bar,-foo")); + // mismatch + assert(test("std.foo", "-bar,-banana")); + + // test combination (exclusion has precedence) + assert(!test("std.foo", "+foo,-foo")); + assert(test("std.foo", "+foo,-bar")); + assert(test("std.bar.foo", "-barr,+bar")); +} diff --git a/src/dscanner/analysis/static_if_else.d b/src/dscanner/analysis/static_if_else.d index f5d03b06..6be61807 100644 --- a/src/dscanner/analysis/static_if_else.d +++ b/src/dscanner/analysis/static_if_else.d @@ -5,10 +5,10 @@ module dscanner.analysis.static_if_else; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.utils : safeAccess; +import dmd.tokens : Token, TOK; +import std.algorithm; +import std.array; /** * Checks for potentially mistaken static if / else if. @@ -19,96 +19,160 @@ import dscanner.utils : safeAccess; * } else if (bar) { * } * --- - * + * * However, it's more likely that this is a mistake. */ -final class StaticIfElse : BaseAnalyzer +extern (C++) class StaticIfElse(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"static_if_else_check"; - this(BaseAnalyzerArguments args) + private Token[] tokens; + + private enum KEY = "dscanner.suspicious.static_if_else"; + private enum MESSAGE = "Mismatched static if. Use 'else static if' here."; + + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); + lexFile(); } - override void visit(const ConditionalStatement cc) + private void lexFile() { - cc.accept(this); - if (cc.falseStatement is null) - { - return; - } - const(IfStatement) ifStmt = getIfStatement(cc); - if (!ifStmt) - { + import dscanner.utils : readFile; + import dmd.errorsink : ErrorSinkNull; + import dmd.globals : global; + import dmd.lexer : Lexer; + + auto bytes = readFile(fileName) ~ '\0'; + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope lexer = new Lexer(null, cast(char*) bytes, 0, bytes.length, 0, 0, 1, errorSinkNull, &global.compileEnv); + while (lexer.nextToken() != TOK.endOfFile) + tokens ~= lexer.token; + } + + override void visit(AST.UserAttributeDeclaration userAttribute) + { + if (shouldIgnoreDecl(userAttribute, KEY)) return; - } - auto tokens = ifStmt.tokens[0 .. 1]; - // extend one token to include `else` before this if - tokens = (tokens.ptr - 1)[0 .. 2]; - addErrorMessage(tokens, KEY, "Mismatched static if. Use 'else static if' here.", - [ - AutoFix.insertionBefore(tokens[$ - 1], "static "), - AutoFix.resolveLater("Wrap '{}' block around 'if'", [tokens[0].index, ifStmt.tokens[$ - 1].index, 0]) - ]); + + super.visit(userAttribute); } - const(IfStatement) getIfStatement(const ConditionalStatement cc) + override void visit(AST.Module mod) { - return safeAccess(cc).falseStatement.statement.statementNoCaseNoDefault.ifStatement; + if (shouldIgnoreDecl(mod.userAttribDecl(), KEY)) + return; + + super.visit(mod); } - override AutoFix.CodeReplacement[] resolveAutoFix( - const Module mod, - scope const(Token)[] tokens, - const AutoFix.ResolveContext context, - const AutoFixFormatting formatting, - ) + override void visit(AST.ConditionalStatement s) { - import dscanner.analysis.helpers : getLineIndentation; - import std.algorithm : countUntil; + import std.range : retro; - auto beforeElse = tokens.countUntil!(a => a.index == context.params[0]); - auto lastToken = tokens.countUntil!(a => a.index == context.params[1]); - if (beforeElse == -1 || lastToken == -1) - throw new Exception("got different tokens than what was used to generate this autofix"); + if (!s.condition.isStaticIfCondition()) + { + super.visit(s); + return; + } - auto indentation = getLineIndentation(tokens, tokens[beforeElse].line, formatting); + s.condition.accept(this); - string beforeIf = formatting.getWhitespaceBeforeOpeningBrace(indentation, false) - ~ "{" ~ formatting.eol ~ indentation; - string afterIf = formatting.eol ~ indentation ~ "}"; + if (s.ifbody) + s.ifbody.accept(this); + + if (s.elsebody) + { + if (auto ifStmt = s.elsebody.isIfStatement()) + { + auto tokenRange = tokens.filter!(t => t.loc.linnum >= s.loc.linnum) + .filter!(t => t.loc.fileOffset <= ifStmt.endloc.fileOffset); + + auto tabSize = tokenRange + .until!(t => t.value == TOK.else_) + .array + .retro() + .until!(t => t.value != TOK.whitespace) + .count!(t => t.ptr[0] == '\t'); + + string lineTerminator = "\n"; + version (Windows) + { + lineTerminator = "\r\n"; + } - return AutoFix.replacement([tokens[beforeElse].index + 4, tokens[beforeElse + 1].index], beforeIf, "") - .concat(AutoFix.indentLines(tokens[beforeElse + 1 .. lastToken + 1], formatting)) - .concat(AutoFix.insertionAfter(tokens[lastToken], afterIf)) - .expectReplacements; - } + string braceStart = " {" ~ lineTerminator ~ "\t"; + string braceEnd = "}" ~ lineTerminator; + for (int i = 0; i < tabSize - 1; i++) + { + braceStart ~= '\t'; + braceEnd ~= '\t'; + } + braceStart ~= '\t'; + + auto fileOffsets = tokenRange.find!(t => t.value == TOK.else_) + .filter!(t => t.ptr[0] == '\n') + .map!(t => t.loc.fileOffset + 1) + .array; + + AutoFix autofix2 = + AutoFix.insertionAt(ifStmt.endloc.fileOffset, braceEnd, "Wrap '{}' block around 'if'"); + foreach (fileOffset; fileOffsets) + autofix2 = autofix2.concat(AutoFix.insertionAt(fileOffset, "\t")); + autofix2 = autofix2.concat(AutoFix.insertionAt(ifStmt.loc.fileOffset, braceStart)); + + auto ifRange = tokenRange.find!(t => t.loc.fileOffset >= ifStmt.ifbody.loc.fileOffset) + .array; + if (ifRange[0].value == TOK.leftCurly) + { + int idx = 1; + while (ifRange[idx].value == TOK.whitespace) + idx++; + autofix2 = autofix2.concat(AutoFix.insertionAt(ifRange[idx].loc.fileOffset, "\t")); + } + else + { + autofix2 = autofix2.concat(AutoFix.insertionAt(ifStmt.ifbody.loc.fileOffset, "\t")); + } - enum KEY = "dscanner.suspicious.static_if_else"; + ulong[2] index = [cast(ulong) s.elsebody.loc.fileOffset - 5, cast(ulong) ifStmt.loc.fileOffset + 2]; + ulong[2] lines = [cast(ulong) s.elsebody.loc.linnum, cast(ulong) ifStmt.loc.linnum]; + ulong[2] columns = [cast(ulong) s.elsebody.loc.charnum, cast(ulong) ifStmt.loc.charnum + 2]; + addErrorMessage( + index, lines, columns, KEY, MESSAGE, + [AutoFix.insertionAt(ifStmt.loc.fileOffset, "static "), autofix2] + ); + } + + s.elsebody.accept(this); + } + } } unittest { - import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD, assertAutoFix; + import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.static_if_else_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo() { static if (false) auto a = 0; - else if (true) /+ - ^^^^^^^ [warn]: Mismatched static if. Use 'else static if' here. +/ + else if (true) // [warn]: Mismatched static if. Use 'else static if' here. auto b = 1; } }c, sac); + // Explicit braces, so no warning. - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void foo() { static if (false) auto a = 0; diff --git a/src/dscanner/analysis/stats_collector.d b/src/dscanner/analysis/stats_collector.d index 4d69730b..5ba7208b 100644 --- a/src/dscanner/analysis/stats_collector.d +++ b/src/dscanner/analysis/stats_collector.d @@ -5,62 +5,154 @@ module dscanner.analysis.stats_collector; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -final class StatsCollector : BaseAnalyzer +extern (C++) class StatsCollector(AST) : BaseAnalyzerDmd { - alias visit = ASTVisitor.visit; + alias visit = BaseAnalyzerDmd.visit; + mixin AnalyzerInfo!"stats_collector"; - this(BaseAnalyzerArguments args) + public uint interfaceCount; + public uint classCount; + public uint functionCount; + public uint templateCount; + public uint structCount; + public uint statementCount; + // TODO: Count lines of code + public uint lineOfCodeCount; + // TODO: Count undocumented public symbols + public uint undocumentedPublicSymbols; + + extern (D) this(string fileName = "", bool skipTests = false) { - args.skipTests = false; // old behavior compatibility - super(args); + super(fileName, skipTests); } - override void visit(const Statement statement) + override void visit(AST.InterfaceDeclaration interfaceDecl) { - statementCount++; - statement.accept(this); + interfaceCount++; + super.visit(interfaceDecl); } - override void visit(const ClassDeclaration classDeclaration) + override void visit(AST.ClassDeclaration classDecl) { classCount++; - classDeclaration.accept(this); + super.visit(classDecl); } - override void visit(const InterfaceDeclaration interfaceDeclaration) + override void visit(AST.FuncDeclaration funcDecl) { - interfaceCount++; - interfaceDeclaration.accept(this); + functionCount++; + super.visit(funcDecl); } - override void visit(const FunctionDeclaration functionDeclaration) + override void visit(AST.TemplateDeclaration templateDecl) { - functionCount++; - functionDeclaration.accept(this); + templateCount++; + super.visit(templateDecl); } - override void visit(const StructDeclaration structDeclaration) + override void visit(AST.StructDeclaration structDecl) { structCount++; - structDeclaration.accept(this); + super.visit(structDecl); } - override void visit(const TemplateDeclaration templateDeclaration) + mixin VisitStatement!(AST.ErrorStatement); + mixin VisitStatement!(AST.PeelStatement); + mixin VisitStatement!(AST.ScopeStatement); + mixin VisitStatement!(AST.ExpStatement); + mixin VisitStatement!(AST.ReturnStatement); + mixin VisitStatement!(AST.IfStatement); + mixin VisitStatement!(AST.CaseStatement); + mixin VisitStatement!(AST.DefaultStatement); + mixin VisitStatement!(AST.LabelStatement); + mixin VisitStatement!(AST.GotoStatement); + mixin VisitStatement!(AST.GotoDefaultStatement); + mixin VisitStatement!(AST.GotoCaseStatement); + mixin VisitStatement!(AST.BreakStatement); + mixin VisitStatement!(AST.DtorExpStatement); + mixin VisitStatement!(AST.MixinStatement); + mixin VisitStatement!(AST.ForwardingStatement); + mixin VisitStatement!(AST.DoStatement); + mixin VisitStatement!(AST.WhileStatement); + mixin VisitStatement!(AST.ForStatement); + mixin VisitStatement!(AST.ForeachStatement); + mixin VisitStatement!(AST.SwitchStatement); + mixin VisitStatement!(AST.ContinueStatement); + mixin VisitStatement!(AST.WithStatement); + mixin VisitStatement!(AST.TryCatchStatement); + mixin VisitStatement!(AST.ThrowStatement); + mixin VisitStatement!(AST.DebugStatement); + mixin VisitStatement!(AST.TryFinallyStatement); + mixin VisitStatement!(AST.ScopeGuardStatement); + mixin VisitStatement!(AST.SwitchErrorStatement); + mixin VisitStatement!(AST.UnrolledLoopStatement); + mixin VisitStatement!(AST.ForeachRangeStatement); + mixin VisitStatement!(AST.CompoundDeclarationStatement); + mixin VisitStatement!(AST.CompoundAsmStatement); + mixin VisitStatement!(AST.StaticAssertStatement); + mixin VisitStatement!(AST.CaseRangeStatement); + mixin VisitStatement!(AST.SynchronizedStatement); + mixin VisitStatement!(AST.AsmStatement); + mixin VisitStatement!(AST.InlineAsmStatement); + mixin VisitStatement!(AST.GccAsmStatement); + mixin VisitStatement!(AST.ImportStatement); + + private template VisitStatement(NodeType) { - templateCount++; - templateDeclaration.accept(this); + override void visit(NodeType node) + { + statementCount++; + super.visit(node); + } + } +} + +unittest +{ + import std.file : exists, remove; + import std.path : dirName; + import std.stdio : File, stderr; + import dscanner.analysis.rundmd : parseDmdModule; + import dmd.astcodegen : ASTCodegen; + + string code = q{ + interface I {} + class C {} + void f() {} + template T() {} + struct S {} + + void funcWithStatements() + { + int a = 1; + if (a == 1) + a = 2; + a++; + } + }c; + + auto testFileName = "test.d"; + File f = File(testFileName, "w"); + scope(exit) + { + assert(exists(testFileName)); + remove(testFileName); } - uint interfaceCount; - uint classCount; - uint functionCount; - uint templateCount; - uint structCount; - uint statementCount; - uint lineOfCodeCount; - uint undocumentedPublicSymbols; + f.rawWrite(code); + f.close(); + auto dmdModule = parseDmdModule(testFileName, code); + auto collector = new StatsCollector!ASTCodegen(); + dmdModule.accept(collector); + + assert(collector.interfaceCount == 1); + assert(collector.classCount == 1); + assert(collector.functionCount == 2); + assert(collector.templateCount == 1); + assert(collector.structCount == 1); + assert(collector.statementCount == 4); + + stderr.writeln("Unittest for StatsCollector passed."); } diff --git a/src/dscanner/analysis/style.d b/src/dscanner/analysis/style.d index 1c3ee39a..42347d6c 100644 --- a/src/dscanner/analysis/style.d +++ b/src/dscanner/analysis/style.d @@ -5,194 +5,150 @@ module dscanner.analysis.style; -import std.stdio; -import dparse.ast; -import dparse.lexer; -import std.regex; -import std.array; -import std.conv; -import std.format; -import dscanner.analysis.helpers; import dscanner.analysis.base; -import dscanner.analysis.nolint; -import dsymbol.scope_ : Scope; +import dmd.astenums : LINK; +import dmd.location : Loc; +import std.conv : to; +import std.format : format; +import std.regex; -final class StyleChecker : BaseAnalyzer +extern (C++) class StyleChecker(AST) : BaseAnalyzerDmd { - alias visit = ASTVisitor.visit; - - enum string varFunNameRegex = `^([\p{Ll}_][_\w\d]*|[\p{Lu}\d_]+)$`; - enum string aggregateNameRegex = `^\p{Lu}[\w\d]*$`; - enum string moduleNameRegex = `^[\p{Ll}_\d]+$`; - enum string KEY = "dscanner.style.phobos_naming_convention"; mixin AnalyzerInfo!"style_check"; + alias visit = BaseAnalyzerDmd.visit; + + private enum KEY = "dscanner.suspicious.style_check"; + private enum MSG = "%s name '%s' does not match style guidelines."; + + private enum varFunNameRegex = `^([\p{Ll}_][_\w\d]*|[\p{Lu}\d_]+)$`; + private enum aggregateNameRegex = `^\p{Lu}[\w\d]*$`; + private enum moduleNameRegex = `^[\p{Ll}_\d]+$`; - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const ModuleDeclaration dec) + override void visit(AST.Module moduleNode) { - with (noLint.push(NoLintFactory.fromModuleDeclaration(dec))) - dec.accept(this); + if (shouldIgnoreDecl(moduleNode.userAttribDecl(), KEY)) + return; - foreach (part; dec.moduleName.identifiers) - { - if (part.text.matchFirst(moduleNameRegex).length == 0) - addErrorMessage(part, KEY, - "Module/package name '" ~ part.text ~ "' does not match style guidelines."); - } - } + super.visit(moduleNode); - // "extern (Windows) {}" : push visit pop - override void visit(const Declaration dec) - { - bool p; - if (dec.attributes) - foreach (attrib; dec.attributes) - if (const LinkageAttribute la = attrib.linkageAttribute) - { - p = true; - pushWinStyle(la.identifier.text.length && la.identifier.text == "Windows"); - } + if (moduleNode.md is null) + return; - dec.accept(this); + auto moduleDecl = *moduleNode.md; + auto moduleName = cast(string) moduleDecl.id.toString(); - if (p) - popWinStyle; - } + if (moduleName.matchFirst(moduleNameRegex).length == 0) + addError(moduleDecl.loc, "Module/package", moduleName); - // "extern (Windows) :" : overwrite current - override void visit(const AttributeDeclaration dec) - { - if (dec.attribute && dec.attribute.linkageAttribute) + foreach (pkg; moduleDecl.packages) { - const LinkageAttribute la = dec.attribute.linkageAttribute; - _winStyles[$-1] = la.identifier.text.length && la.identifier.text == "Windows"; - } - } - - override void visit(const VariableDeclaration vd) - { - vd.accept(this); - } + auto pkgName = pkg.toString(); - override void visit(const Declarator dec) - { - checkLowercaseName("Variable", dec.name); + if (pkgName.matchFirst(moduleNameRegex).length == 0) + addError(moduleDecl.loc, "Module/package", moduleName); + } } - override void visit(const FunctionDeclaration dec) + override void visit(AST.LinkDeclaration linkDeclaration) { - // "extern(Windows) Name();" push visit pop - bool p; - if (dec.attributes) - foreach (attrib; dec.attributes) - if (const LinkageAttribute la = attrib.linkageAttribute) - { - p = true; - pushWinStyle(la.identifier.text.length && la.identifier.text == "Windows"); - } - - if (dec.functionBody.specifiedFunctionBody || - (dec.functionBody.missingFunctionBody && !winStyle())) - checkLowercaseName("Function", dec.name); + if (linkDeclaration.decl is null) + return; - if (p) - popWinStyle; + foreach (symbol; *linkDeclaration.decl) + if (!isWindowsFunctionWithNoBody(symbol, linkDeclaration.linkage)) + symbol.accept(this); } - void checkLowercaseName(string type, ref const Token name) + private bool isWindowsFunctionWithNoBody(AST.Dsymbol symbol, LINK linkage) { - if (name.text.length > 0 && name.text.matchFirst(varFunNameRegex).length == 0) - addErrorMessage(name, KEY, - type ~ " name '" ~ name.text ~ "' does not match style guidelines."); + auto fd = symbol.isFuncDeclaration(); + return linkage == LINK.windows && fd && !fd.fbody; } - override void visit(const ClassDeclaration dec) + override void visit(AST.VarDeclaration varDeclaration) { - checkAggregateName("Class", dec.name); - dec.accept(this); - } + import dmd.astenums : STC; - override void visit(const InterfaceDeclaration dec) - { - checkAggregateName("Interface", dec.name); - dec.accept(this); - } + super.visit(varDeclaration); - override void visit(const EnumDeclaration dec) - { - if (dec.name.text is null || dec.name.text.length == 0) + if (varDeclaration.storage_class & STC.manifest || varDeclaration.ident is null) return; - checkAggregateName("Enum", dec.name); - dec.accept(this); - } - override void visit(const StructDeclaration dec) - { - checkAggregateName("Struct", dec.name); - dec.accept(this); - } + auto varName = cast(string) varDeclaration.ident.toString(); - void checkAggregateName(string aggregateType, ref const Token name) - { - if (name.text.length > 0 && name.text.matchFirst(aggregateNameRegex).length == 0) - addErrorMessage(name, KEY, - aggregateType ~ " name '" ~ name.text ~ "' does not match style guidelines."); + if (varName.matchFirst(varFunNameRegex).length == 0) + addError(varDeclaration.loc, "Variable", varName); } - bool[] _winStyles = [false]; + mixin VisitNode!(AST.ClassDeclaration, "Class", aggregateNameRegex); + mixin VisitNode!(AST.StructDeclaration, "Struct", aggregateNameRegex); + mixin VisitNode!(AST.InterfaceDeclaration, "Interface", aggregateNameRegex); + mixin VisitNode!(AST.UnionDeclaration, "Union", aggregateNameRegex); + mixin VisitNode!(AST.EnumDeclaration, "Enum", aggregateNameRegex); + mixin VisitNode!(AST.FuncDeclaration, "Function", varFunNameRegex); + mixin VisitNode!(AST.TemplateDeclaration, "Template", varFunNameRegex); - bool winStyle() + private template VisitNode(NodeType, string nodeName, string regex) { - return _winStyles[$-1]; - } + override void visit(NodeType node) + { + super.visit(node); - void pushWinStyle(const bool value) - { - _winStyles.length += 1; - _winStyles[$-1] = value; + if (node.ident is null) + return; + + auto nodeSymbolName = cast(string) node.ident.toString(); + + if (nodeSymbolName.matchFirst(regex).length == 0) + addError(node.loc, nodeName, nodeSymbolName); + } } - void popWinStyle() + private extern (D) void addError(Loc loc, string nodeType, string nodeName) { - _winStyles.length -= 1; + auto fileOffset = cast(ulong) loc.fileOffset; + auto lineNum = cast(ulong) loc.linnum; + auto charNum = cast(ulong) loc.charnum; + ulong[2] index = [fileOffset, fileOffset + nodeName.length]; + ulong[2] lines = [lineNum, lineNum]; + ulong[2] columns = [charNum, charNum + nodeName.length]; + addErrorMessage(index, lines, columns, KEY, MSG.format(nodeType, nodeName)); } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.style_check = Check.enabled; - assertAnalyzerWarnings(q{ - module AMODULE; /+ - ^^^^^^^ [warn]: Module/package name 'AMODULE' does not match style guidelines. +/ + assertAnalyzerWarningsDMD(q{ + module AMODULE; // [warn]: Module/package name 'AMODULE' does not match style guidelines. bool A_VARIABLE; // FIXME: bool a_variable; // ok bool aVariable; // ok void A_FUNCTION() {} // FIXME: - class cat {} /+ - ^^^ [warn]: Class name 'cat' does not match style guidelines. +/ - interface puma {} /+ - ^^^^ [warn]: Interface name 'puma' does not match style guidelines. +/ - struct dog {} /+ - ^^^ [warn]: Struct name 'dog' does not match style guidelines. +/ - enum racoon { a } /+ - ^^^^^^ [warn]: Enum name 'racoon' does not match style guidelines. +/ + class cat {} // [warn]: Class name 'cat' does not match style guidelines. + interface puma {} // [warn]: Interface name 'puma' does not match style guidelines. + struct dog {} // [warn]: Struct name 'dog' does not match style guidelines. + enum racoon { a } // [warn]: Enum name 'racoon' does not match style guidelines. enum bool something = false; enum bool someThing = false; enum Cat { fritz, } enum Cat = Cat.fritz; }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ extern(Windows) { bool Fun0(); @@ -200,40 +156,35 @@ unittest } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ extern(Windows) { - extern(D) bool Fun2(); /+ - ^^^^ [warn]: Function name 'Fun2' does not match style guidelines. +/ + extern(D) bool Fun2(); // [warn]: Function name 'Fun2' does not match style guidelines. bool Fun3(); } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ extern(Windows) { extern(C): - extern(D) bool Fun4(); /+ - ^^^^ [warn]: Function name 'Fun4' does not match style guidelines. +/ - bool Fun5(); /+ - ^^^^ [warn]: Function name 'Fun5' does not match style guidelines. +/ + extern(D) bool Fun4(); // [warn]: Function name 'Fun4' does not match style guidelines. + bool Fun5(); // [warn]: Function name 'Fun5' does not match style guidelines. } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ extern(Windows): bool Fun6(); bool Fun7(); extern(D): void okOkay(); - void NotReallyOkay(); /+ - ^^^^^^^^^^^^^ [warn]: Function name 'NotReallyOkay' does not match style guidelines. +/ + void NotReallyOkay(); // [warn]: Function name 'NotReallyOkay' does not match style guidelines. }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ extern(Windows): - bool WinButWithBody(){} /+ - ^^^^^^^^^^^^^^ [warn]: Function name 'WinButWithBody' does not match style guidelines. +/ + bool WinButWithBody(){} // [warn]: Function name 'WinButWithBody' does not match style guidelines. }c, sac); stderr.writeln("Unittest for StyleChecker passed."); diff --git a/src/dscanner/analysis/trust_too_much.d b/src/dscanner/analysis/trust_too_much.d index c9648266..e589136c 100644 --- a/src/dscanner/analysis/trust_too_much.d +++ b/src/dscanner/analysis/trust_too_much.d @@ -5,103 +5,65 @@ module dscanner.analysis.trust_too_much; -import std.stdio; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dsymbol.scope_; +import dmd.astenums : STC; /** * Checks that `@trusted` is only applied to a a single function */ -final class TrustTooMuchCheck : BaseAnalyzer +extern(C++) class TrustTooMuchCheck(AST) : BaseAnalyzerDmd { -private: + mixin AnalyzerInfo!"trust_too_much"; + alias visit = BaseAnalyzerDmd.visit; - static immutable MESSAGE = "Trusting a whole scope is a bad idea, " ~ +private: + extern(D) static immutable MESSAGE = "Trusting a whole scope is a bad idea, " ~ "`@trusted` should only be attached to the functions individually"; - static immutable string KEY = "dscanner.trust_too_much"; - - bool checkAtAttribute = true; + extern(D) static immutable string KEY = "dscanner.trust_too_much"; public: - - alias visit = BaseAnalyzer.visit; - - mixin AnalyzerInfo!"trust_too_much"; - /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const AtAttribute d) + override void visit(AST.StorageClassDeclaration scd) { - if (checkAtAttribute && d.identifier.text == "trusted") - addErrorMessage(d, KEY, MESSAGE); - d.accept(this); - } - - // always applied to function body, so OK - override void visit(const MemberFunctionAttribute d) - { - const oldCheckAtAttribute = checkAtAttribute; - checkAtAttribute = false; - d.accept(this); - checkAtAttribute = oldCheckAtAttribute; - } - - // handles `@trusted{}` and old style, leading, atAttribute for single funcs - override void visit(const Declaration d) - { - const oldCheckAtAttribute = checkAtAttribute; - - checkAtAttribute = d.functionDeclaration is null && d.unittest_ is null && - d.constructor is null && d.destructor is null && - d.staticConstructor is null && d.staticDestructor is null && - d.sharedStaticConstructor is null && d.sharedStaticDestructor is null; - d.accept(this); - checkAtAttribute = oldCheckAtAttribute; - } - - // issue #588 - override void visit(const AliasDeclaration d) - { - const oldCheckAtAttribute = checkAtAttribute; - checkAtAttribute = false; - d.accept(this); - checkAtAttribute = oldCheckAtAttribute; + if (scd.stc & STC.trusted) + addErrorMessage(cast(ulong) scd.loc.linnum, cast(ulong) scd.loc.charnum, + KEY, MESSAGE); + + super.visit(scd); } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarnings = assertAnalyzerWarningsDMD; import std.format : format; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.trust_too_much = Check.enabled; - const msg = TrustTooMuchCheck.MESSAGE; + const msg = "Trusting a whole scope is a bad idea, " ~ + "`@trusted` should only be attached to the functions individually"; //--- fail cases ---// assertAnalyzerWarnings(q{ - @trusted: /+ - ^^^^^^^^ [warn]: %s +/ + @trusted: // [warn]: %s void test(); }c.format(msg), sac); assertAnalyzerWarnings(q{ - @trusted @nogc: /+ - ^^^^^^^^ [warn]: %s +/ + @trusted @nogc: // [warn]: %s void test(); }c.format(msg), sac); assertAnalyzerWarnings(q{ - @trusted { /+ - ^^^^^^^^ [warn]: %s +/ + @trusted { // [warn]: %s void test(); void test(); } @@ -109,31 +71,27 @@ unittest assertAnalyzerWarnings(q{ @safe { - @trusted @nogc { /+ - ^^^^^^^^ [warn]: %s +/ + @trusted @nogc { // [warn]: %s void test(); void test(); }} }c.format(msg), sac); assertAnalyzerWarnings(q{ - @nogc @trusted { /+ - ^^^^^^^^ [warn]: %s +/ + @nogc @trusted { // [warn]: %s void test(); void test(); } }c.format(msg), sac); assertAnalyzerWarnings(q{ - @trusted template foo(){ /+ - ^^^^^^^^ [warn]: %s +/ + @trusted template foo(){ // [warn]: %s } }c.format(msg), sac); assertAnalyzerWarnings(q{ struct foo{ - @trusted: /+ - ^^^^^^^^ [warn]: %s +/ + @trusted: // [warn]: %s } }c.format(msg), sac); //--- pass cases ---// @@ -161,4 +119,4 @@ unittest }c , sac); stderr.writeln("Unittest for TrustTooMuchCheck passed."); -} +} \ No newline at end of file diff --git a/src/dscanner/analysis/undocumented.d b/src/dscanner/analysis/undocumented.d index 6e44281b..5760d2fb 100644 --- a/src/dscanner/analysis/undocumented.d +++ b/src/dscanner/analysis/undocumented.d @@ -6,338 +6,220 @@ module dscanner.analysis.undocumented; import dscanner.analysis.base; -import dsymbol.scope_ : Scope; -import dparse.ast; -import dparse.lexer; - +import dmd.astenums : STC; +import std.format : format; import std.regex : ctRegex, matchAll; -import std.stdio; /** * Checks for undocumented public declarations. Ignores some operator overloads, * main functions, and functions whose name starts with "get" or "set". */ -final class UndocumentedDeclarationCheck : BaseAnalyzer +extern (C++) class UndocumentedDeclarationCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"undocumented_declaration_check"; - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.style.undocumented_declaration"; + private enum DEFAULT_MSG = "Public declaration is undocumented."; + private enum MSG = "Public declaration '%s' is undocumented."; + + private immutable string[] ignoredFunctionNames = [ + "opCmp", "opEquals", "toString", "toHash", "main" + ]; + private enum getSetRe = ctRegex!`^(?:get|set)(?:\p{Lu}|_).*`; + + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const Module mod) + override void visit(AST.VisibilityDeclaration visibilityDecl) { - push(tok!"public"); - mod.accept(this); + import dmd.dsymbol : Visibility; + + if (visibilityDecl.visibility.kind == Visibility.Kind.public_) + super.visit(visibilityDecl); } - override void visit(const Declaration dec) + override void visit(AST.StorageClassDeclaration storageClassDecl) { - if (dec.attributeDeclaration) - { - auto attr = dec.attributeDeclaration.attribute; - if (isProtection(attr.attribute.type)) - set(attr.attribute.type); - else if (attr.attribute == tok!"override") - setOverride(true); - else if (attr.deprecated_ !is null) - setDeprecated(true); - else if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable") - setDisabled(true); - } - - immutable bool shouldPop = dec.attributeDeclaration is null; - immutable bool prevOverride = getOverride(); - immutable bool prevDisabled = getDisabled(); - immutable bool prevDeprecated = getDeprecated(); - bool dis; - bool dep; - bool ovr; - bool pushed; - foreach (attribute; dec.attributes) - { - if (isProtection(attribute.attribute.type)) - { - if (shouldPop) - { - pushed = true; - push(attribute.attribute.type); - } - else - set(attribute.attribute.type); - } - else if (attribute.attribute == tok!"override") - ovr = true; - else if (attribute.deprecated_ !is null) - dep = true; - else if (attribute.atAttribute !is null - && attribute.atAttribute.identifier.text == "disable") - dis = true; - } - if (ovr) - setOverride(true); - if (dis) - setDisabled(true); - if (dep) - setDeprecated(true); - dec.accept(this); - if (shouldPop && pushed) - pop(); - if (ovr) - setOverride(prevOverride); - if (dis) - setDisabled(prevDisabled); - if (dep) - setDeprecated(prevDeprecated); + if (!hasIgnorableStorageClass(storageClassDecl.stc)) + super.visit(storageClassDecl); } - override void visit(const VariableDeclaration variable) + override void visit(AST.DeprecatedDeclaration _) {} + + override void visit(AST.FuncDeclaration funcDecl) { - if (!currentIsInteresting() || variable.comment.ptr !is null) + if (funcDecl.comment() !is null || funcDecl.ident is null) return; - if (variable.autoDeclaration !is null) - { - addMessage(variable.autoDeclaration.parts[0].identifier, - variable.autoDeclaration.parts[0].identifier.text); - return; - } - foreach (dec; variable.declarators) + + string funcName = cast(string) funcDecl.ident.toString(); + bool canBeUndocumented = hasIgnorableStorageClass(funcDecl.storage_class) || isIgnorableFunctionName(funcName); + + if (!canBeUndocumented) { - if (dec.comment.ptr is null) - addMessage(dec.name, dec.name.text); - return; + addErrorMessage(funcDecl.loc.linnum, funcDecl.loc.charnum, KEY, MSG.format(funcName)); + super.visit(funcDecl); } } - override void visit(const ConditionalDeclaration cond) + private extern (D) bool isIgnorableFunctionName(string funcName) { - const VersionCondition ver = cond.compileCondition.versionCondition; - if (ver is null || (ver.token != tok!"unittest" && ver.token.text != "none")) - cond.accept(this); - else if (cond.falseDeclarations.length > 0) - foreach (f; cond.falseDeclarations) - visit(f); - } + import std.algorithm : canFind; - override void visit(const FunctionBody fb) - { + return ignoredFunctionNames.canFind(funcName) || !matchAll(funcName, getSetRe).empty; } - override void visit(const Unittest u) + override void visit(AST.CtorDeclaration ctorDecl) { - } + if (ctorDecl.comment() !is null) + return; - override void visit(const TraitsExpression t) - { + addErrorMessage(ctorDecl.loc.linnum, ctorDecl.loc.charnum, KEY, DEFAULT_MSG); } - mixin V!AnonymousEnumMember; - mixin V!ClassDeclaration; - mixin V!EnumDeclaration; - mixin V!InterfaceDeclaration; - mixin V!StructDeclaration; - mixin V!UnionDeclaration; - mixin V!TemplateDeclaration; - mixin V!FunctionDeclaration; - mixin V!Constructor; + override void visit(AST.TemplateDeclaration templateDecl) + { + if (templateDecl.comment() !is null || templateDecl.ident is null) + return; -private: + if (!templateDecl.isDeprecated()) + { + string templateName = cast(string) templateDecl.ident.toString(); + addErrorMessage(templateDecl.loc.linnum, templateDecl.loc.charnum, KEY, MSG.format(templateName)); + } + } - enum string KEY = "dscanner.style.undocumented_declaration"; + mixin VisitDeclaration!(AST.ClassDeclaration); + mixin VisitDeclaration!(AST.InterfaceDeclaration); + mixin VisitDeclaration!(AST.StructDeclaration); + mixin VisitDeclaration!(AST.UnionDeclaration); + mixin VisitDeclaration!(AST.EnumDeclaration); + mixin VisitDeclaration!(AST.EnumMember); + mixin VisitDeclaration!(AST.VarDeclaration); - mixin template V(T) + private template VisitDeclaration(NodeType) { - override void visit(const T declaration) + override void visit(NodeType decl) { - import std.traits : hasMember; - static if (hasMember!(T, "storageClasses")) + if (decl.comment() !is null || decl.ident is null) { - // stop at declarations with a deprecated in their storage classes - foreach (sc; declaration.storageClasses) - if (sc.deprecated_ !is null) - return; + super.visit(decl); + return; } - if (currentIsInteresting()) + bool canBeUndocumented; + static if (__traits(hasMember, NodeType, "storage_class")) + canBeUndocumented = hasIgnorableStorageClass(decl.storage_class); + + if (!canBeUndocumented) { - if (declaration.comment.ptr is null) - { - static if (hasMember!(T, "name")) - { - static if (is(T == FunctionDeclaration)) - { - import std.algorithm : canFind; - - if (!(ignoredFunctionNames.canFind(declaration.name.text) - || isGetterOrSetter(declaration.name.text) - || isProperty(declaration))) - { - addMessage(declaration.name, declaration.name.text); - } - } - else - { - if (declaration.name.type != tok!"") - addMessage(declaration.name, declaration.name.text); - } - } - else - { - import std.algorithm : countUntil; - - // like constructors - auto tokens = declaration.tokens.findTokenForDisplay(tok!"this"); - auto earlyEnd = tokens.countUntil!(a => a == tok!"{" || a == tok!"(" || a == tok!";"); - if (earlyEnd != -1) - tokens = tokens[0 .. earlyEnd]; - addMessage(tokens, null); - } - } - static if (!(is(T == TemplateDeclaration) || is(T == FunctionDeclaration))) - { - declaration.accept(this); - } + string declName = cast(string) decl.ident.toString(); + addErrorMessage(decl.loc.linnum, decl.loc.charnum, KEY, MSG.format(declName)); + super.visit(decl); } } } - static bool isGetterOrSetter(string name) + private bool hasIgnorableStorageClass(ulong storageClass) { - return !matchAll(name, getSetRe).empty; + return (storageClass & STC.deprecated_) || (storageClass & STC.override_) + || (storageClass & STC.disable) || (storageClass & STC.property); } - static bool isProperty(const FunctionDeclaration dec) - { - if (dec.memberFunctionAttributes is null) - return false; - foreach (attr; dec.memberFunctionAttributes) - { - if (attr.atAttribute && attr.atAttribute.identifier.text == "property") - return true; - } - return false; - } - - void addMessage(T)(T range, string name) - { - import std.string : format; + override void visit(AST.UnitTestDeclaration _) {} - addErrorMessage(range, KEY, name is null - ? "Public declaration is undocumented." - : format("Public declaration '%s' is undocumented.", name)); - } + override void visit(AST.TraitsExp _) {} - bool getOverride() + override void visit(AST.ConditionalDeclaration conditionalDecl) { - return stack[$ - 1].isOverride; - } + auto versionCond = conditionalDecl.condition.isVersionCondition(); - void setOverride(bool o = true) - { - stack[$ - 1].isOverride = o; - } + if (versionCond is null) + super.visit(conditionalDecl); - bool getDisabled() - { - return stack[$ - 1].isDisabled; - } - - void setDisabled(bool d = true) - { - stack[$ - 1].isDisabled = d; - } - - bool getDeprecated() - { - return stack[$ - 1].isDeprecated; + if (isIgnorableVersion(versionCond) && conditionalDecl.elsedecl) + { + foreach (decl; *(conditionalDecl.elsedecl)) + super.visit(decl); + } } - void setDeprecated(bool d = true) + override void visit(AST.ConditionalStatement conditionalStatement) { - stack[$ - 1].isDeprecated = d; - } + auto versionCond = conditionalStatement.condition.isVersionCondition(); - bool currentIsInteresting() - { - return stack[$ - 1].protection == tok!"public" - && !getOverride() && !getDisabled() && !getDeprecated(); - } + if (versionCond is null) + super.visit(conditionalStatement); - void set(IdType p) - in - { - assert(isProtection(p)); - } - do - { - stack[$ - 1].protection = p; + if (isIgnorableVersion(versionCond) && conditionalStatement.elsebody) + { + super.visit(conditionalStatement.elsebody); + } } - void push(IdType p) - in - { - assert(isProtection(p)); - } - do + private bool isIgnorableVersion(AST.VersionCondition versionCond) { - stack ~= ProtectionInfo(p, false); - } + if (versionCond is null || versionCond.ident is null) + return false; - void pop() - { - assert(stack.length > 1); - stack = stack[0 .. $ - 1]; - } + string versionStr = cast(string) versionCond.ident.toString(); - static struct ProtectionInfo - { - IdType protection; - bool isOverride; - bool isDeprecated; - bool isDisabled; + return versionStr == "unittest" || versionStr == "none"; } - - ProtectionInfo[] stack; } -// Ignore undocumented symbols with these names -private immutable string[] ignoredFunctionNames = [ - "opCmp", "opEquals", "toString", "toHash", "main" -]; - -private enum getSetRe = ctRegex!`^(?:get|set)(?:\p{Lu}|_).*`; - unittest { import std.stdio : stderr; - import std.format : format; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.undocumented_declaration_check = Check.enabled; - assertAnalyzerWarnings(q{ - class C{} /+ - ^ [warn]: Public declaration 'C' is undocumented. +/ - interface I{} /+ - ^ [warn]: Public declaration 'I' is undocumented. +/ - enum e = 0; /+ - ^ [warn]: Public declaration 'e' is undocumented. +/ - void f(){} /+ - ^ [warn]: Public declaration 'f' is undocumented. +/ - struct S{} /+ - ^ [warn]: Public declaration 'S' is undocumented. +/ - template T(){} /+ - ^ [warn]: Public declaration 'T' is undocumented. +/ - union U{} /+ - ^ [warn]: Public declaration 'U' is undocumented. +/ + assertAnalyzerWarningsDMD(q{ + private int x; + int y; // [warn]: Public declaration 'y' is undocumented. + public int z; // [warn]: Public declaration 'z' is undocumented. + + /// + class C + { + int h; // [warn]: Public declaration 'h' is undocumented. + + public: + int g; // [warn]: Public declaration 'g' is undocumented. + void f() {} // [warn]: Public declaration 'f' is undocumented. + + private: + int a; + int b; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + deprecated int y; + + /// + class C + { + private int b; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + class C{} // [warn]: Public declaration 'C' is undocumented. + interface I{} // [warn]: Public declaration 'I' is undocumented. + enum e = 0; // [warn]: Public declaration 'e' is undocumented. + void f(){} // [warn]: Public declaration 'f' is undocumented. + struct S{} // [warn]: Public declaration 'S' is undocumented. + template T(){} // [warn]: Public declaration 'T' is undocumented. + union U{} // [warn]: Public declaration 'U' is undocumented. }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ /// C class C{} /// I @@ -355,12 +237,13 @@ unittest }, sac); // https://github.com/dlang-community/D-Scanner/issues/760 - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ + deprecated("This has been deprecated") auto func(){} deprecated auto func(){} deprecated auto func()(){} }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class C{} /// a interface I{} /// b enum e = 0; /// c @@ -370,5 +253,34 @@ unittest union U{} /// g }, sac); + assertAnalyzerWarningsDMD(q{ + int x; // [warn]: Public declaration 'x' is undocumented. + int y; /// + + /// + class C + { + private int a; + int b; /// + int c; // [warn]: Public declaration 'c' is undocumented. + protected int d; + } + + /// + class D + { + /// + void fun() + { + class Inner + { + int z; + } + + int a1, a2, a3; + } + } + }c, sac); + stderr.writeln("Unittest for UndocumentedDeclarationCheck passed."); } diff --git a/src/dscanner/analysis/unmodified.d b/src/dscanner/analysis/unmodified.d index 6dd3d8c1..7fcef713 100644 --- a/src/dscanner/analysis/unmodified.d +++ b/src/dscanner/analysis/unmodified.d @@ -5,378 +5,311 @@ module dscanner.analysis.unmodified; import dscanner.analysis.base; -import dscanner.analysis.nolint; -import dscanner.utils : safeAccess; -import dsymbol.scope_ : Scope; -import std.container; -import dparse.ast; -import dparse.lexer; /** * Checks for variables that could have been declared const or immutable */ -final class UnmodifiedFinder : BaseAnalyzer +// TODO: many similarities to unused_param.d, maybe refactor into a common base class +extern (C++) class UnmodifiedFinder(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"could_be_immutable_check"; - /// - this(BaseAnalyzerArguments args) - { - super(args); - } + private enum KEY = "dscanner.suspicious.unmodified"; + private enum MSG = "Variable %s is never modified and could have been declared const or immutable."; - override void visit(const Module mod) + private static struct VarInfo { - pushScope(); - mod.accept(this); - popScope(); + string name; + ulong lineNum; + ulong charNum; + bool isUsed = false; } - override void visit(const BlockStatement blockStatement) - { - pushScope(); - blockStatementDepth++; - blockStatement.accept(this); - blockStatementDepth--; - popScope(); - } + private alias VarSet = VarInfo[string]; + private VarSet[] usedVars; + private bool inAggregate; - override void visit(const StructBody structBody) + extern (D) this(string fileName, bool skipTests = false) { + super(fileName, skipTests); pushScope(); - immutable oldBlockStatementDepth = blockStatementDepth; - blockStatementDepth = 0; - structBody.accept(this); - blockStatementDepth = oldBlockStatementDepth; - popScope(); } - override void visit(const VariableDeclaration dec) + override void visit(AST.UserAttributeDeclaration userAttribute) { - if (dec.autoDeclaration is null && blockStatementDepth > 0 - && isImmutable <= 0 && !canFindImmutable(dec)) - { - foreach (d; dec.declarators) - { - if (initializedFromCast(d.initializer)) - continue; - if (initializedFromNew(d.initializer)) - continue; - tree[$ - 1].insert(new VariableInfo(d.name.text, d.name, isValueTypeSimple(dec.type))); - } - } - dec.accept(this); + if (shouldIgnoreDecl(userAttribute, KEY)) + return; + + super.visit(userAttribute); } - override void visit(const AutoDeclaration autoDeclaration) + override void visit(AST.Module mod) { - import std.algorithm : canFind; + if (shouldIgnoreDecl(mod.userAttribDecl(), KEY)) + return; - if (blockStatementDepth > 0 && isImmutable <= 0 - && (!autoDeclaration.storageClasses.canFind!(a => a.token == tok!"const" - || a.token == tok!"enum" || a.token == tok!"immutable"))) - { - foreach (part; autoDeclaration.parts) - { - if (initializedFromCast(part.initializer)) - continue; - if (initializedFromNew(part.initializer)) - continue; - tree[$ - 1].insert(new VariableInfo(part.identifier.text, part.identifier)); - } - } - autoDeclaration.accept(this); + super.visit(mod); } - override void visit(const AssignExpression assignExpression) + override void visit(AST.CompoundStatement compoundStatement) { - if (assignExpression.operator != tok!"") - { - interest++; - guaranteeUse++; - assignExpression.ternaryExpression.accept(this); - guaranteeUse--; - interest--; - - if (assignExpression.operator == tok!"~=") - interest++; - assignExpression.expression.accept(this); - if (assignExpression.operator == tok!"~=") - interest--; - } - else - assignExpression.accept(this); + pushScope(); + super.visit(compoundStatement); + popScope(); } - override void visit(const Declaration dec) + override void visit(AST.TemplateDeclaration templateDeclaration) { - if (canFindImmutableOrConst(dec)) - { - isImmutable++; - with (noLint.push(NoLintFactory.fromDeclaration(dec))) - dec.accept(this); - isImmutable--; - } - else - { - with (noLint.push(NoLintFactory.fromDeclaration(dec))) - dec.accept(this); - } + auto oldInTemplate = inAggregate; + inAggregate = true; + super.visit(templateDeclaration); + inAggregate = oldInTemplate; } - override void visit(const IdentifierChain ic) + override void visit(AST.StructDeclaration structDecl) { - if (ic.identifiers.length && interest > 0) - variableMightBeModified(ic.identifiers[0].text); - ic.accept(this); + auto oldInAggregate = inAggregate; + inAggregate = true; + super.visit(structDecl); + inAggregate = oldInAggregate; } - override void visit(const IdentifierOrTemplateInstance ioti) + override void visit(AST.VarDeclaration varDeclaration) { - if (ioti.identifier != tok!"" && interest > 0) - variableMightBeModified(ioti.identifier.text); - ioti.accept(this); - } + import dmd.astenums : STC; - mixin PartsMightModify!AsmPrimaryExp; - mixin PartsMightModify!IndexExpression; - mixin PartsMightModify!FunctionCallExpression; - mixin PartsMightModify!NewExpression; - mixin PartsMightModify!IdentifierOrTemplateChain; - mixin PartsMightModify!ReturnStatement; + super.visit(varDeclaration); - override void visit(const UnaryExpression unary) - { - if (unary.prefix == tok!"++" || unary.prefix == tok!"--" - || unary.suffix == tok!"++" || unary.suffix == tok!"--" - || unary.prefix == tok!"*" || unary.prefix == tok!"&") - { - interest++; - guaranteeUse++; - unary.accept(this); - guaranteeUse--; - interest--; - } - else - unary.accept(this); - } + if (varDeclaration.ident is null) + return; - override void visit(const ForeachStatement foreachStatement) - { - if (foreachStatement.low !is null) - { - interest++; - foreachStatement.low.accept(this); - interest--; - } - if (foreachStatement.declarationOrStatement !is null) - foreachStatement.declarationOrStatement.accept(this); + string varName = cast(string) varDeclaration.ident.toString(); + bool isConst = varDeclaration.storage_class & STC.const_ || varDeclaration.storage_class & STC.immutable_ + || varDeclaration.storage_class & STC.manifest || isConstType(varDeclaration.type); + + bool markAsUsed = isConst || isFromCastOrNew(varDeclaration._init) || inAggregate; + currentScope[varName] = VarInfo(varName, varDeclaration.loc.linnum, varDeclaration.loc.charnum, markAsUsed); } - override void visit(const TraitsExpression) + private bool isConstType(AST.Type type) { - // issue #266: Ignore unmodified variables inside of `__traits` expressions + import dmd.astenums : MODFlags; + + if (type is null) + return false; + + bool isConst = type.mod & MODFlags.const_ || type.mod & MODFlags.immutable_; + + if (auto typePtr = type.isTypePointer()) + isConst = isConst || typePtr.next.mod & MODFlags.const_ || typePtr.next.mod & MODFlags.immutable_; + + return isConst; } - override void visit(const TypeofExpression) + private bool isFromCastOrNew(AST.Initializer initializer) { - // issue #270: Ignore unmodified variables inside of `typeof` expressions + if (initializer is null) + return false; + + auto initExpr = initializer.isExpInitializer(); + if (initExpr is null) + return false; + + return initExpr.exp.isNewExp() !is null || initExpr.exp.isCastExp() !is null; } - override void visit(const AsmStatement a) + override void visit(AST.IntervalExp intervalExp) { - inAsm = true; - a.accept(this); - inAsm = false; - } + super.visit(intervalExp); -private: + auto identifier1 = intervalExp.lwr.isIdentifierExp(); + if (identifier1 && identifier1.ident) + markAsUsed(cast(string) identifier1.ident.toString()); - enum string KEY = "dscanner.suspicious.unmodified"; + auto identifier2 = intervalExp.upr.isIdentifierExp(); + if (identifier2 && identifier2.ident) + markAsUsed(cast(string) identifier2.ident.toString()); + } - template PartsMightModify(T) + override void visit(AST.IndexExp indexExpression) { - override void visit(const T t) - { - interest++; - t.accept(this); - interest--; - } + super.visit(indexExpression); + + auto identifier1 = indexExpression.e1.isIdentifierExp(); + if (identifier1 && identifier1.ident) + markAsUsed(cast(string) identifier1.ident.toString()); + + auto identifier2 = indexExpression.e2.isIdentifierExp(); + if (identifier2 && identifier2.ident) + markAsUsed(cast(string) identifier2.ident.toString()); } - void variableMightBeModified(string name) + mixin VisitAssignNode!(AST.AssignExp); + mixin VisitAssignNode!(AST.BinAssignExp); + mixin VisitAssignNode!(AST.PtrExp); + mixin VisitAssignNode!(AST.AddrExp); + mixin VisitAssignNode!(AST.PreExp); + mixin VisitAssignNode!(AST.PostExp); + + private template VisitAssignNode(NodeType) { - size_t index = tree.length - 1; - auto vi = VariableInfo(name); - if (guaranteeUse == 0) + override void visit(NodeType node) { - auto r = tree[index].equalRange(&vi); - if (!r.empty && r.front.isValueType && !inAsm) + super.visit(node); + + if (node.e1 is null) return; - } - while (true) - { - if (tree[index].removeKey(&vi) != 0 || index == 0) - break; - index--; + + auto identifier = node.e1.isIdentifierExp(); + if (identifier && identifier.ident) + markAsUsed(cast(string) identifier.ident.toString()); } } - bool initializedFromNew(const Initializer initializer) + mixin VisitFunctionNode!(AST.CallExp); + mixin VisitFunctionNode!(AST.NewExp); + + private template VisitFunctionNode(NodeType) { - if (const UnaryExpression ue = cast(UnaryExpression) safeAccess(initializer) - .nonVoidInitializer.assignExpression) + override void visit(NodeType node) { - return ue.newExpression !is null; - } - return false; - } + super.visit(node); - bool initializedFromCast(const Initializer initializer) - { - import std.typecons : scoped; + if (node.arguments is null) + return; - static class CastFinder : ASTVisitor - { - alias visit = ASTVisitor.visit; - override void visit(const CastExpression castExpression) + foreach (arg; *node.arguments) { - foundCast = true; - castExpression.accept(this); + auto identifier = arg.isIdentifierExp(); + if (identifier && identifier.ident) + markAsUsed(cast(string) arg.isIdentifierExp().ident.toString()); } - - bool foundCast; } - - if (initializer is null) - return false; - auto finder = scoped!CastFinder(); - finder.visit(initializer); - return finder.foundCast; } - bool canFindImmutableOrConst(const Declaration dec) - { - import std.algorithm : canFind, map, filter; + mixin VisitDotExpressionNode!(AST.DotIdExp); + mixin VisitDotExpressionNode!(AST.DotTemplateInstanceExp); - return !dec.attributes.map!(a => a.attribute) - .filter!(a => a == tok!"immutable" || a == tok!"const").empty; + private template VisitDotExpressionNode(NodeType) + { + override void visit(NodeType node) + { + super.visit(node); + auto identifierExp = node.e1.isIdentifierExp(); + if (identifierExp && identifierExp.ident) + markAsUsed(cast(string) identifierExp.ident.toString()); + } } - bool canFindImmutable(const VariableDeclaration dec) + private extern (D) void markAsUsed(string varName) { - import std.algorithm : canFind; + import std.range : retro; - foreach (storageClass; dec.storageClasses) + foreach (funcScope; usedVars.retro()) { - if (storageClass.token == tok!"enum") - return true; - } - foreach (sc; dec.storageClasses) - { - if (sc.token == tok!"immutable" || sc.token == tok!"const") - return true; - } - if (dec.type !is null) - { - foreach (tk; dec.type.typeConstructors) - if (tk == tok!"immutable" || tk == tok!"const") - return true; - if (dec.type.type2) + if (varName in funcScope) { - const tk = dec.type.type2.typeConstructor; - if (tk == tok!"immutable" || tk == tok!"const") - return true; + funcScope[varName].isUsed = true; + break; } } - return false; } - static struct VariableInfo + @property private extern (D) VarSet currentScope() { - string name; - Token token; - bool isValueType; + return usedVars[$ - 1]; } - void popScope() + private void pushScope() { - foreach (vi; tree[$ - 1]) - { - immutable string errorMessage = "Variable " ~ vi.name - ~ " is never modified and could have been declared const or immutable."; - addErrorMessage(vi.token, KEY, errorMessage); - } - tree = tree[0 .. $ - 1]; + // Error with gdc-12 + //usedVars ~= new VarSet; + + // Workaround for gdc-12 + VarSet newScope; + newScope["test"] = VarInfo("test", 0, 0); + usedVars ~= newScope; + currentScope.remove("test"); } - void pushScope() + private void popScope() { - tree ~= new RedBlackTree!(VariableInfo*, "a.name < b.name"); - } - - int blockStatementDepth; - - int interest; + import std.algorithm : each, filter; + import std.format : format; - int guaranteeUse; + currentScope.byValue + .filter!(var => !var.isUsed) + .each!(var => addErrorMessage(var.lineNum, var.charNum, KEY, MSG.format(var.name))); - int isImmutable; - - bool inAsm; - - RedBlackTree!(VariableInfo*, "a.name < b.name")[] tree; -} - -bool isValueTypeSimple(const Type type) pure nothrow @nogc -{ - if (type.type2 is null) - return false; - return type.type2.builtinType != tok!"" && type.typeSuffixes.length == 0; + usedVars.length--; + } } -@system unittest +unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; - import std.format : format; StaticAnalysisConfig sac = disabledConfig(); sac.could_be_immutable_check = Check.enabled; // fails - - assertAnalyzerWarnings(q{ - void foo(){int i = 1;} /+ - ^ [warn]: Variable i is never modified and could have been declared const or immutable. +/ + assertAnalyzerWarningsDMD(q{ + void foo() + { + int i = 1; // [warn]: Variable i is never modified and could have been declared const or immutable. + } }, sac); - // pass - - assertAnalyzerWarnings(q{ - void foo(){const(int) i;} - }, sac); + assertAnalyzerWarningsDMD(q{ + void foo() + { + int i = 5; // [warn]: Variable i is never modified and could have been declared const or immutable. + int j = 6; + j = i + 5; + } + }c, sac); - assertAnalyzerWarnings(q{ - void foo(){immutable(int)* i;} + // pass + assertAnalyzerWarningsDMD(q{ + void foo() + { + const(int) i; + const int j; + const(int)* a; + const int* b; + } }, sac); - assertAnalyzerWarnings(q{ - void foo(){enum i = 1;} + assertAnalyzerWarningsDMD(q{ + void foo() + { + immutable(int) i; + immutable int j; + immutable(int)* b; + immutable int* a; + } }, sac); - assertAnalyzerWarnings(q{ - void foo(){E e = new E;} + assertAnalyzerWarningsDMD(q{ + void foo() + { + enum i = 1; + enum string j = "test"; + } }, sac); - assertAnalyzerWarnings(q{ - void foo(){auto e = new E;} + assertAnalyzerWarningsDMD(q{ + void foo() + { + E e = new E; + auto f = new F; + } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void issue640() { size_t i1; @@ -387,11 +320,32 @@ bool isValueTypeSimple(const Type type) pure nothrow @nogc } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ + void foo() + { + int i = 5; // [warn]: Variable i is never modified and could have been declared const or immutable. + int j = 6; + j = i + 5; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + void foo() + { + int i = 5; + if (true) + --i; + else + i++; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ @("nolint(dscanner.suspicious.unmodified)") void foo(){ int i = 1; } }, sac); -} + stderr.writeln("Unittest for UnmodifiedFinder passed."); +} diff --git a/src/dscanner/analysis/unused.d b/src/dscanner/analysis/unused.d deleted file mode 100644 index 9089134f..00000000 --- a/src/dscanner/analysis/unused.d +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright Brian Schott (Hackerpilot) 2014-2015. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) -module dscanner.analysis.unused; - -import dparse.ast; -import dparse.lexer; -import dscanner.analysis.base; -import std.container; -import std.regex : Regex, regex, matchAll; -import dsymbol.scope_ : Scope; -import std.algorithm : all; - -/** - * Checks for unused variables. - */ -abstract class UnusedIdentifierCheck : BaseAnalyzer -{ - alias visit = BaseAnalyzer.visit; - - /** - */ - this(BaseAnalyzerArguments args) - { - super(args); - re = regex("[\\p{Alphabetic}_][\\w_]*"); - } - - override void visit(const Module mod) - { - pushScope(); - mod.accept(this); - popScope(); - } - - override void visit(const Declaration declaration) - { - if (!isOverride) - foreach (attribute; declaration.attributes) - isOverride = isOverride || (attribute.attribute == tok!"override"); - declaration.accept(this); - isOverride = false; - } - - override void visit(const FunctionDeclaration functionDec) - { - pushScope(); - if (functionDec.functionBody - && (functionDec.functionBody.specifiedFunctionBody - || functionDec.functionBody.shortenedFunctionBody)) - { - immutable bool ias = inAggregateScope; - inAggregateScope = false; - if (!isOverride) - functionDec.parameters.accept(this); - functionDec.functionBody.accept(this); - inAggregateScope = ias; - } - popScope(); - } - - mixin PartsUseVariables!AliasInitializer; - mixin PartsUseVariables!ArgumentList; - mixin PartsUseVariables!AssertExpression; - mixin PartsUseVariables!ClassDeclaration; - mixin PartsUseVariables!FunctionBody; - mixin PartsUseVariables!FunctionCallExpression; - mixin PartsUseVariables!FunctionDeclaration; - mixin PartsUseVariables!IndexExpression; - mixin PartsUseVariables!Initializer; - mixin PartsUseVariables!InterfaceDeclaration; - mixin PartsUseVariables!NewExpression; - mixin PartsUseVariables!StaticIfCondition; - mixin PartsUseVariables!StructDeclaration; - mixin PartsUseVariables!TemplateArgumentList; - mixin PartsUseVariables!ThrowExpression; - mixin PartsUseVariables!CastExpression; - - override void dynamicDispatch(const ExpressionNode n) - { - interestDepth++; - super.dynamicDispatch(n); - interestDepth--; - } - - override void visit(const SwitchStatement switchStatement) - { - if (switchStatement.expression !is null) - { - interestDepth++; - switchStatement.expression.accept(this); - interestDepth--; - } - switchStatement.accept(this); - } - - override void visit(const WhileStatement whileStatement) - { - if (whileStatement.condition.expression !is null) - { - interestDepth++; - whileStatement.condition.expression.accept(this); - interestDepth--; - } - if (whileStatement.declarationOrStatement !is null) - whileStatement.declarationOrStatement.accept(this); - } - - override void visit(const DoStatement doStatement) - { - if (doStatement.expression !is null) - { - interestDepth++; - doStatement.expression.accept(this); - interestDepth--; - } - if (doStatement.statementNoCaseNoDefault !is null) - doStatement.statementNoCaseNoDefault.accept(this); - } - - override void visit(const ForStatement forStatement) - { - if (forStatement.initialization !is null) - forStatement.initialization.accept(this); - if (forStatement.test !is null) - { - interestDepth++; - forStatement.test.accept(this); - interestDepth--; - } - if (forStatement.increment !is null) - { - interestDepth++; - forStatement.increment.accept(this); - interestDepth--; - } - if (forStatement.declarationOrStatement !is null) - forStatement.declarationOrStatement.accept(this); - } - - override void visit(const IfStatement ifStatement) - { - if (ifStatement.condition.expression !is null) - { - interestDepth++; - ifStatement.condition.expression.accept(this); - interestDepth--; - } - if (ifStatement.thenStatement !is null) - ifStatement.thenStatement.accept(this); - if (ifStatement.elseStatement !is null) - ifStatement.elseStatement.accept(this); - } - - override void visit(const ForeachStatement foreachStatement) - { - if (foreachStatement.low !is null) - { - interestDepth++; - foreachStatement.low.accept(this); - interestDepth--; - } - if (foreachStatement.high !is null) - { - interestDepth++; - foreachStatement.high.accept(this); - interestDepth--; - } - foreachStatement.accept(this); - } - - override void visit(const AssignExpression assignExp) - { - interestDepth++; - assignExp.accept(this); - interestDepth--; - } - - override void visit(const TemplateDeclaration templateDeclaration) - { - immutable inAgg = inAggregateScope; - inAggregateScope = true; - templateDeclaration.accept(this); - inAggregateScope = inAgg; - } - - override void visit(const IdentifierOrTemplateChain chain) - { - if (interestDepth > 0 && chain.identifiersOrTemplateInstances[0].identifier != tok!"") - variableUsed(chain.identifiersOrTemplateInstances[0].identifier.text); - chain.accept(this); - } - - override void visit(const TemplateSingleArgument single) - { - if (single.token != tok!"") - variableUsed(single.token.text); - } - - override void visit(const UnaryExpression unary) - { - const bool interesting = unary.prefix == tok!"*" || unary.unaryExpression !is null; - interestDepth += interesting; - unary.accept(this); - interestDepth -= interesting; - } - - override void visit(const MixinExpression mix) - { - interestDepth++; - mixinDepth++; - mix.accept(this); - mixinDepth--; - interestDepth--; - } - - override void visit(const PrimaryExpression primary) - { - if (interestDepth > 0) - { - const IdentifierOrTemplateInstance idt = primary.identifierOrTemplateInstance; - - if (idt !is null) - { - if (idt.identifier != tok!"") - variableUsed(idt.identifier.text); - else if (idt.templateInstance && idt.templateInstance.identifier != tok!"") - variableUsed(idt.templateInstance.identifier.text); - } - if (mixinDepth > 0 && primary.primary == tok!"stringLiteral" - || primary.primary == tok!"wstringLiteral" - || primary.primary == tok!"dstringLiteral") - { - foreach (part; matchAll(primary.primary.text, re)) - { - void checkTree(in size_t treeIndex) - { - auto uu = UnUsed(part.hit); - auto r = tree[treeIndex].equalRange(&uu); - if (!r.empty) - r.front.uncertain = true; - } - checkTree(tree.length - 1); - if (tree.length >= 2) - checkTree(tree.length - 2); - } - } - } - primary.accept(this); - } - - override void visit(const ReturnStatement retStatement) - { - if (retStatement.expression !is null) - { - interestDepth++; - visit(retStatement.expression); - interestDepth--; - } - } - - override void visit(const BlockStatement blockStatement) - { - immutable bool sb = inAggregateScope; - inAggregateScope = false; - if (blockStatementIntroducesScope) - pushScope(); - blockStatement.accept(this); - if (blockStatementIntroducesScope) - popScope(); - inAggregateScope = sb; - } - - override void visit(const Type2 tp) - { - if (tp.typeIdentifierPart && - tp.typeIdentifierPart.identifierOrTemplateInstance) - { - const IdentifierOrTemplateInstance idt = tp.typeIdentifierPart.identifierOrTemplateInstance; - if (idt.identifier != tok!"") - variableUsed(idt.identifier.text); - else if (idt.templateInstance) - { - const TemplateInstance ti = idt.templateInstance; - if (ti.identifier != tok!"") - variableUsed(idt.templateInstance.identifier.text); - if (ti.templateArguments && ti.templateArguments.templateSingleArgument) - variableUsed(ti.templateArguments.templateSingleArgument.token.text); - } - } - tp.accept(this); - } - - override void visit(const WithStatement withStatetement) - { - interestDepth++; - if (withStatetement.expression) - withStatetement.expression.accept(this); - interestDepth--; - if (withStatetement.declarationOrStatement) - withStatetement.declarationOrStatement.accept(this); - } - - override void visit(const StructBody structBody) - { - immutable bool sb = inAggregateScope; - inAggregateScope = true; - foreach (dec; structBody.declarations) - visit(dec); - inAggregateScope = sb; - } - - override void visit(const ConditionalStatement conditionalStatement) - { - immutable bool cs = blockStatementIntroducesScope; - blockStatementIntroducesScope = false; - conditionalStatement.accept(this); - blockStatementIntroducesScope = cs; - } - - override void visit(const AsmPrimaryExp primary) - { - if (primary.token != tok!"") - variableUsed(primary.token.text); - if (primary.identifierChain !is null) - variableUsed(primary.identifierChain.identifiers[0].text); - } - - override void visit(const TraitsExpression) - { - // issue #266: Ignore unused variables inside of `__traits` expressions - } - - override void visit(const TypeofExpression) - { - // issue #270: Ignore unused variables inside of `typeof` expressions - } - - abstract protected void popScope(); - - protected uint interestDepth; - - protected Tree[] tree; - - protected void variableDeclared(string name, Token token, bool isRef) - { - if (inAggregateScope || name.all!(a => a == '_')) - return; - tree[$ - 1].insert(new UnUsed(name, token, isRef)); - } - - protected void pushScope() - { - tree ~= new Tree; - } - -private: - - struct UnUsed - { - string name; - Token token; - bool isRef; - bool uncertain; - } - - alias Tree = RedBlackTree!(UnUsed*, "a.name < b.name"); - - mixin template PartsUseVariables(NodeType) - { - override void visit(const NodeType node) - { - interestDepth++; - node.accept(this); - interestDepth--; - } - } - - void variableUsed(string name) - { - size_t treeIndex = tree.length - 1; - auto uu = UnUsed(name); - while (true) - { - if (tree[treeIndex].removeKey(&uu) != 0 || treeIndex == 0) - break; - treeIndex--; - } - } - - Regex!char re; - - bool inAggregateScope; - - uint mixinDepth; - - bool isOverride; - - bool blockStatementIntroducesScope = true; -} - -/// Base class for unused parameter/variables checks -abstract class UnusedStorageCheck : UnusedIdentifierCheck -{ - alias visit = UnusedIdentifierCheck.visit; - - /** - Ignore declarations which are allowed to be unused, e.g. inside of a - speculative compilation: __traits(compiles, { S s = 0; }) - **/ - uint ignoreDeclarations = 0; - - /// Kind of declaration for error messages e.g. "Variable" - const string publicType; - - /// Kind of declaration for error reports e.g. "unused_variable" - const string reportType; - - /** - * Params: - * args = commonly shared analyzer arguments - * publicType = declaration kind used in error messages, e.g. "Variable"s - * reportType = declaration kind used in error reports, e.g. "unused_variable" - */ - this(BaseAnalyzerArguments args, string publicType = null, string reportType = null) - { - super(args); - this.publicType = publicType; - this.reportType = reportType; - } - - override void visit(const TraitsExpression traitsExp) - { - // issue #788: Enum values might be used inside of `__traits` expressions, e.g.: - // enum name = "abc"; - // __traits(hasMember, S, name); - ignoreDeclarations++; - if (traitsExp.templateArgumentList) - traitsExp.templateArgumentList.accept(this); - ignoreDeclarations--; - } - - override final protected void popScope() - { - if (!ignoreDeclarations) - { - foreach (uu; tree[$ - 1]) - { - if (!uu.isRef && tree.length > 1) - { - if (uu.uncertain) - continue; - immutable string errorMessage = publicType ~ ' ' ~ uu.name ~ " is never used."; - addErrorMessage(uu.token, "dscanner.suspicious." ~ reportType, errorMessage); - } - } - } - tree = tree[0 .. $ - 1]; - } -} diff --git a/src/dscanner/analysis/unused_label.d b/src/dscanner/analysis/unused_label.d index fac4174a..37a232da 100644 --- a/src/dscanner/analysis/unused_label.d +++ b/src/dscanner/analysis/unused_label.d @@ -5,128 +5,131 @@ module dscanner.analysis.unused_label; import dscanner.analysis.base; -import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; -import dsymbol.scope_ : Scope; -import std.algorithm.iteration : each; +import dmd.tokens; /** * Checks for labels that are never used. */ -final class UnusedLabelCheck : BaseAnalyzer +extern (C++) class UnusedLabelCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"unused_label_check"; - /// - this(BaseAnalyzerArguments args) + extern (D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const Module mod) + override void visit(AST.Module m) { pushScope(); - mod.accept(this); + super.visit(m); popScope(); } - override void visit(const FunctionLiteralExpression flit) + override void visit(AST.LabelStatement ls) { - if (flit.specifiedFunctionBody) + Label* label = ls.ident.toString() in current; + + if (label is null) + { + current[ls.ident.toString()] = Label(ls.ident.toString(), + ls.loc.linnum, ls.loc.charnum, false); + } + else { - pushScope(); - flit.specifiedFunctionBody.accept(this); - popScope(); + label.line = ls.loc.linnum; + label.column = ls.loc.charnum; } + + super.visit(ls); } - override void visit(const FunctionBody functionBody) + override void visit(AST.GotoStatement gs) { - if (functionBody.specifiedFunctionBody !is null) - { - pushScope(); - functionBody.specifiedFunctionBody.accept(this); - popScope(); - } - if (functionBody.missingFunctionBody && functionBody.missingFunctionBody.functionContracts) - functionBody.missingFunctionBody.functionContracts.each!((a){pushScope(); a.accept(this); popScope();}); + if (gs.ident) + labelUsed(gs.ident.toString()); } - override void visit(const LabeledStatement labeledStatement) + override void visit(AST.BreakStatement bs) { - auto token = labeledStatement.identifier; - Label* label = token.text in current; - if (label is null) - { - current[token.text] = Label(token.text, token, false); - } - else - { - label.token = token; - } - if (labeledStatement.declarationOrStatement !is null) - labeledStatement.declarationOrStatement.accept(this); + if (bs.ident) + labelUsed(bs.ident.toString()); + } + + override void visit(AST.StaticForeachStatement s) + { + if (s.sfe.aggrfe) + super.visit(s.sfe.aggrfe); + + if (s.sfe.rangefe) + super.visit(s.sfe.rangefe); } - override void visit(const ContinueStatement contStatement) + override void visit(AST.ContinueStatement cs) { - if (contStatement.label.text.length) - labelUsed(contStatement.label.text); + if (cs.ident) + labelUsed(cs.ident.toString()); } - override void visit(const BreakStatement breakStatement) + override void visit(AST.FuncDeclaration fd) { - if (breakStatement.label.text.length) - labelUsed(breakStatement.label.text); + pushScope(); + super.visit(fd); + popScope(); } - override void visit(const GotoStatement gotoStatement) + override void visit(AST.FuncLiteralDeclaration fd) { - if (gotoStatement.label.text.length) - labelUsed(gotoStatement.label.text); + pushScope(); + super.visit(fd); + popScope(); } - override void visit(const AsmInstruction instr) + override void visit(AST.AsmStatement as) { - instr.accept(this); + if (!as.tokens) + return; + // Look for jump instructions bool jmp; - if (instr.identifierOrIntegerOrOpcode.text.length) - jmp = instr.identifierOrIntegerOrOpcode.text[0] == 'j'; + if (getFirstLetterOf(cast(char*) as.tokens[0].ptr) == 'j') + jmp = true; - if (!jmp || !instr.operands || instr.operands.operands.length != 1) - return; + // Last argument of the jmp instruction will be the label + Token* label; + for (label = as.tokens; label.next; label = label.next) {} - const AsmExp e = cast(AsmExp) instr.operands.operands[0]; - if (e.left && cast(AsmBrExp) e.left) - { - const AsmBrExp b = cast(AsmBrExp) e.left; - if (b && b.asmUnaExp && b.asmUnaExp.asmPrimaryExp) - { - const AsmPrimaryExp p = b.asmUnaExp.asmPrimaryExp; - if (p && p.identifierChain && p.identifierChain.identifiers.length == 1) - labelUsed(p.identifierChain.identifiers[0].text); - } - } + if (jmp && label.ident) + labelUsed(label.ident.toString()); } -private: + private char getFirstLetterOf(char* str) + { + import std.ascii : isAlpha; + + if (str is null) + return '\0'; - enum string KEY = "dscanner.suspicious.unused_label"; + while (str && !isAlpha(*str)) + str++; + + return *str; + } + +private: static struct Label { - string name; - Token token; + const(char)[] name; + size_t line; + size_t column; bool used; } - Label[string][] stack; + extern (D) Label[const(char)[]][] stack; - auto ref current() + extern (D) auto ref current() { return stack[$ - 1]; } @@ -138,26 +141,28 @@ private: void popScope() { + import std.conv : to; + foreach (label; current.byValue()) { - if (label.token is Token.init) + if (label.line == size_t.max || label.column == size_t.max) { // TODO: handle unknown labels } else if (!label.used) { - addErrorMessage(label.token, KEY, - "Label \"" ~ label.name ~ "\" is not used."); + addErrorMessage(label.line, label.column, "dscanner.suspicious.unused_label", + "Label \"" ~ to!string(label.name) ~ "\" is not used."); } } stack.length--; } - void labelUsed(string name) + extern (D) void labelUsed(const(char)[] name) { Label* entry = name in current; if (entry is null) - current[name] = Label(name, Token.init, true); + current[name] = Label(name, size_t.max, size_t.max, true); else entry.used = true; } @@ -165,25 +170,24 @@ private: unittest { - import dscanner.analysis.config : Check, StaticAnalysisConfig, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.unused_label_check = Check.enabled; - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ int testUnusedLabel() { int x = 0; - A: /+ - ^ [warn]: Label "A" is not used. +/ + A: // [warn]: Label "A" is not used. if (x) goto B; x++; B: goto C; void foo() { - C: /+ - ^ [warn]: Label "C" is not used. +/ + C: // [warn]: Label "C" is not used. return; } C: @@ -193,12 +197,10 @@ unittest D: return; } - D: /+ - ^ [warn]: Label "D" is not used. +/ + D: // [warn]: Label "D" is not used. goto E; () { - E: /+ - ^ [warn]: Label "E" is not used. +/ + E: // [warn]: Label "E" is not used. return; }(); E: @@ -207,15 +209,13 @@ unittest F: return; }(); - F: /+ - ^ [warn]: Label "F" is not used. +/ + F: // [warn]: Label "F" is not used. return x; - G: /+ - ^ [warn]: Label "G" is not used. +/ + G: // [warn]: Label "G" is not used. } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testAsm() { asm { jmp lbl;} @@ -223,17 +223,16 @@ unittest } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void testAsm() { asm { mov RAX,1;} - lbl: /+ - ^^^ [warn]: Label "lbl" is not used. +/ + lbl: // [warn]: Label "lbl" is not used. } }c, sac); // from std.math - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ real polyImpl() { asm { jecxz return_ST; @@ -242,7 +241,7 @@ unittest }c, sac); // a label might be hard to find, e.g. in a mixin - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ real polyImpl() { mixin("return_ST: return 1;"); asm { @@ -251,5 +250,16 @@ unittest } }c, sac); + assertAnalyzerWarningsDMD(q{ + void testAsm() + { + asm nothrow @nogc + { + "movgr2fcsr $r0,%0" : + : "r" (newState & (roundingMask | allExceptions)); + } + } + }c, sac); + stderr.writeln("Unittest for UnusedLabelCheck passed."); } diff --git a/src/dscanner/analysis/unused_parameter.d b/src/dscanner/analysis/unused_parameter.d index 878b5104..fb8b31a4 100644 --- a/src/dscanner/analysis/unused_parameter.d +++ b/src/dscanner/analysis/unused_parameter.d @@ -4,64 +4,172 @@ // http://www.boost.org/LICENSE_1_0.txt) module dscanner.analysis.unused_parameter; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.unused; -import dsymbol.scope_ : Scope; +import dmd.astenums : STC; +import std.algorithm : all, canFind, each, filter, map; +import std.conv : to; /** - * Checks for unused variables. + * Checks for unused function parameters. */ -final class UnusedParameterCheck : UnusedStorageCheck +extern (C++) class UnusedParameterCheck(AST) : BaseAnalyzerDmd { - alias visit = UnusedStorageCheck.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"unused_parameter_check"; - /** - * Params: - * fileName = the name of the file being analyzed - */ - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.suspicious.unused_parameter"; + private enum MSG = "Parameter %s is never used."; + + private static struct ParamInfo + { + string name; + ulong lineNum; + ulong charNum; + bool isUsed = false; + } + + private alias ParamSet = ParamInfo[string]; + private ParamSet[] usedParams; + private bool inMixin; + + extern (D) this(string fileName, bool skipTests = false) + { + super(fileName, skipTests); + pushScope(); + } + + override void visit(AST.FuncDeclaration funcDeclaration) + { + import std.format : format; + + pushScope(); + super.visit(funcDeclaration); + + bool shouldIgnoreWarns = funcDeclaration.fbody is null || funcDeclaration.storage_class & STC.override_; + if (!shouldIgnoreWarns) + currentScope.byValue + .filter!(param => !param.isUsed) + .each!(param => addErrorMessage(param.lineNum, param.charNum, KEY, MSG.format(param.name))); + + popScope(); + } + + override void visit(AST.Parameter parameter) { - super(args, "Parameter", "unused_parameter"); + import dmd.astenums : TY; + + if (parameter.ident is null) + return; + + auto varName = cast(string) parameter.ident.toString(); + bool shouldBeIgnored = varName.all!(c => c == '_') || parameter.storageClass & STC.ref_ + || parameter.storageClass & STC.out_ || parameter.type.ty == TY.Tpointer; + if (!shouldBeIgnored) + currentScope[varName] = ParamInfo(varName, parameter.loc.linnum, parameter.loc.charnum); } - override void visit(const Parameter parameter) + override void visit(AST.TypeSArray newExp) { - import std.algorithm : among; - import std.algorithm.iteration : filter; - import std.range : empty; + if (auto identifierExpression = newExp.dim.isIdentifierExp()) + identifierExpression.accept(this); + } + + override void visit(AST.IdentifierExp identifierExp) + { + if (identifierExp.ident) + markAsUsed(cast(string) identifierExp.ident.toString()); + + super.visit(identifierExp); + } - if (parameter.name != tok!"") + mixin VisitMixin!(AST.MixinExp); + mixin VisitMixin!(AST.MixinStatement); + + private template VisitMixin(NodeType) + { + override void visit(NodeType node) { - immutable bool isRef = !parameter.parameterAttributes - .filter!(a => a.idType.among(tok!"ref", tok!"out")).empty; - immutable bool isPtr = parameter.type && !parameter.type - .typeSuffixes.filter!(a => a.star != tok!"").empty; + inMixin = true; + super.visit(node); + inMixin = false; + } + } + + override void visit(AST.StringExp stringExp) + { + if (!inMixin) + return; + + string str = cast(string) stringExp.toStringz(); + currentScope.byKey + .filter!(param => canFind(str, param)) + .each!(param => markAsUsed(param)); + } + + override void visit(AST.TraitsExp traitsExp) + { + import dmd.dtemplate : isType; + + super.visit(traitsExp); - variableDeclared(parameter.name.text, parameter.name, isRef | isPtr); + if (traitsExp.args is null) + return; - if (parameter.default_ !is null) + (*traitsExp.args).opSlice() + .map!(arg => isType(arg)) + .filter!(type => type !is null) + .map!(type => type.isTypeIdentifier()) + .filter!(typeIdentifier => typeIdentifier !is null) + .each!(typeIdentifier => markAsUsed(cast(string) typeIdentifier.toString())); + } + + private extern (D) void markAsUsed(string varName) + { + import std.range : retro; + + foreach (funcScope; usedParams.retro()) + { + if (varName in funcScope) { - interestDepth++; - parameter.default_.accept(this); - interestDepth--; + funcScope[varName].isUsed = true; + break; } } } + + @property private extern (D) ParamSet currentScope() + { + return usedParams[$ - 1]; + } + + private void pushScope() + { + // Error with gdc-12 + //usedParams ~= new ParamSet; + + // Workaround for gdc-12 + ParamSet newScope; + newScope["test"] = ParamInfo("test", 0, 0); + usedParams ~= newScope; + currentScope.remove("test"); + } + + private void popScope() + { + usedParams.length--; + } } @system unittest { import std.stdio : stderr; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.unused_parameter_check = Check.enabled; - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ // bug encountered after correct DIP 1009 impl in dparse version (StdDdoc) @@ -71,11 +179,9 @@ final class UnusedParameterCheck : UnusedStorageCheck is(StringTypeOf!R)); } - void inPSC(in int a){} /+ - ^ [warn]: Parameter a is never used. +/ + void inPSC(in int a){} // [warn]: Parameter a is never used. - void doStuff(int a, int b) /+ - ^ [warn]: Parameter b is never used. +/ + void doStuff(int a, int b) // [warn]: Parameter b is never used. { return a; } @@ -96,8 +202,7 @@ final class UnusedParameterCheck : UnusedStorageCheck { auto cb1 = delegate(size_t _) {}; cb1(3); - auto cb2 = delegate(size_t a) {}; /+ - ^ [warn]: Parameter a is never used. +/ + auto cb2 = delegate(size_t a) {}; // [warn]: Parameter a is never used. cb2(3); } @@ -118,5 +223,68 @@ final class UnusedParameterCheck : UnusedStorageCheck } }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testMixinExpression(const Declaration decl) + { + foreach (property; possibleDeclarations) + if (auto fn = mixin("decl." ~ property)) + addMessage(fn.name.type ? [fn.name] : fn.tokens, fn.name.text); + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testNestedFunction(int a) + { + int nestedFunction(int b) + { + return a + b; + } + + nestedFunction(5); + } + + void testNestedFunctionShadowing(int a) // [warn]: Parameter a is never used. + { + int nestedFunctionShadowing(int a) + { + return a + 5; + } + + nestedFunction(5); + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + override protected void testOverrideFunction(int a) + { + return; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testRefParam(ref LogEntry payload) + { + return; + } + + void testOutParam(out LogEntry payload) + { + return; + } + + void testPointerParam(LogEntry* payload) + { + return; + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + private char[] testStaticArray(size_t size) @safe pure nothrow + { + return new char[size]; + } + }c, sac); + stderr.writeln("Unittest for UnusedParameterCheck passed."); } diff --git a/src/dscanner/analysis/unused_result.d b/src/dscanner/analysis/unused_result.d index 83e4c646..b5815062 100644 --- a/src/dscanner/analysis/unused_result.d +++ b/src/dscanner/analysis/unused_result.d @@ -4,15 +4,7 @@ // http://www.boost.org/LICENSE_1_0.txt) module dscanner.analysis.unused_result; -import dparse.ast; -import dparse.lexer; import dscanner.analysis.base; -import dscanner.analysis.mismatched_args : IdentVisitor, resolveSymbol; -import dscanner.utils; -import dsymbol.scope_; -import dsymbol.symbol; -import std.algorithm : canFind, countUntil; -import std.range : retro; /** * Checks for function call statements which call non-void functions. @@ -24,105 +16,212 @@ import std.range : retro; * When the return value is intentionally discarded, `cast(void)` can * be prepended to silence the check. */ -final class UnusedResultChecker : BaseAnalyzer +extern (C++) class UnusedResultChecker(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; + alias visit = BaseAnalyzerDmd.visit; + mixin AnalyzerInfo!"unused_result"; - mixin AnalyzerInfo!"unused_result"; + private enum KEY = "dscanner.performance.enum_array_literal"; + private enum string MSG = "Function return value is discarded"; -private: + private bool[string] nonVoidFuncs; + private string[] aggregateStack; - enum string KEY = "dscanner.unused_result"; - enum string MSG = "Function return value is discarded"; + extern (D) this(string fileName, bool skipTests = false) + { + super(fileName, skipTests); + } -public: + private template VisitAggregate(NodeType) + { + override void visit(NodeType aggregate) + { + string name = cast(string) aggregate.ident.toString(); + aggregateStack ~= name; + super.visit(aggregate); + aggregateStack.length -= 1; + } + } - const(DSymbol)* void_; - const(DSymbol)* noreturn_; + mixin VisitAggregate!(AST.StructDeclaration); + mixin VisitAggregate!(AST.ClassDeclaration); - /// - this(BaseAnalyzerArguments args) - { - super(args); - void_ = sc.getSymbolsByName(internString("void"))[0]; - auto symbols = sc.getSymbolsByName(internString("noreturn")); - if (symbols.length > 0) - noreturn_ = symbols[0]; - } + override void visit(AST.FuncDeclaration funcDeclaration) + { + import dmd.astenums : TY; - override void visit(const(ExpressionStatement) decl) - { - import std.typecons : scoped; + auto typeFunc = funcDeclaration.type.isTypeFunction(); + if (typeFunc && typeFunc.next && typeFunc.next.ty != TY.Tvoid && typeFunc.next.ty != TY.Tnoreturn) + { + auto typeIdent = typeFunc.next.isTypeIdentifier(); + bool isNoReturn = typeIdent is null ? false : typeIdent.ident.toString() == "noreturn"; - super.visit(decl); - if (!decl.expression) - return; - if (decl.expression.items.length != 1) - return; - auto ue = cast(UnaryExpression) decl.expression.items[0]; - if (!ue) - return; - auto fce = ue.functionCallExpression; - if (!fce) - return; + if (!isNoReturn) + { + string funcName = buildFullyQualifiedName(cast(string) funcDeclaration.ident.toString()); + nonVoidFuncs[funcName] = true; + } + } - auto identVisitor = scoped!IdentVisitor; - if (fce.unaryExpression !is null) - identVisitor.visit(fce.unaryExpression); - else if (fce.type !is null) - identVisitor.visit(fce.type); + super.visit(funcDeclaration); + } - if (!identVisitor.names.length) - return; + override void visit(AST.AliasDeclaration aliasDecl) + { + import std.algorithm : canFind, endsWith; + import std.array : replace; - const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names); + auto typeIdent = aliasDecl.type.isTypeIdentifier(); + if (typeIdent is null) + return; - if (!symbols.length) - return; + string aliasName = cast(string) aliasDecl.ident.toString(); + string targetName = cast(string) typeIdent.ident.toString(); - foreach (sym; symbols) - { - if (!sym) - return; - if (!sym.type) - return; - if (sym.kind != CompletionKind.functionName) - return; - if (sym.type is void_) - return; - if (noreturn_ && sym.type is noreturn_) - return; - } + foreach(func; nonVoidFuncs.byKey()) + { + if (func.endsWith(targetName) || func.canFind(targetName ~ ".")) + { + string newAliasName = func.replace(targetName, aliasName); + nonVoidFuncs[newAliasName] = true; + } + } + } + + private extern (D) string buildFullyQualifiedName(string funcName) + { + import std.algorithm : fold; + + if (aggregateStack.length == 0) + return funcName; + + return aggregateStack.fold!((a, b) => a ~ "." ~ b) ~ "." ~ funcName; + } + + mixin VisitInstructionBlock!(AST.WhileStatement); + mixin VisitInstructionBlock!(AST.ForStatement); + mixin VisitInstructionBlock!(AST.DoStatement); + mixin VisitInstructionBlock!(AST.ForeachRangeStatement); + mixin VisitInstructionBlock!(AST.ForeachStatement); + mixin VisitInstructionBlock!(AST.SwitchStatement); + mixin VisitInstructionBlock!(AST.SynchronizedStatement); + mixin VisitInstructionBlock!(AST.WithStatement); + mixin VisitInstructionBlock!(AST.TryCatchStatement); + mixin VisitInstructionBlock!(AST.TryFinallyStatement); + + override void visit(AST.CompoundStatement compoundStatement) + { + foreach (statement; *compoundStatement.statements) + { + if (hasUnusedResult(statement)) + { + auto lineNum = cast(ulong) statement.loc.linnum; + auto charNum = cast(ulong) statement.loc.charnum; + addErrorMessage(lineNum, charNum, KEY, MSG); + } + + statement.accept(this); + } + } + + override void visit(AST.IfStatement ifStatement) + { + if (hasUnusedResult(ifStatement.ifbody)) + { + auto lineNum = cast(ulong) ifStatement.ifbody.loc.linnum; + auto charNum = cast(ulong) ifStatement.ifbody.loc.charnum; + addErrorMessage(lineNum, charNum, KEY, MSG); + } + + if (ifStatement.elsebody && hasUnusedResult(ifStatement.elsebody)) + { + auto lineNum = cast(ulong) ifStatement.elsebody.loc.linnum; + auto charNum = cast(ulong) ifStatement.elsebody.loc.charnum; + addErrorMessage(lineNum, charNum, KEY, MSG); + } + + super.visit(ifStatement); + } + + private template VisitInstructionBlock(NodeType) + { + override void visit(NodeType statement) + { + if (hasUnusedResult(statement._body)) + { + auto lineNum = cast(ulong) statement._body.loc.linnum; + auto charNum = cast(ulong) statement._body.loc.charnum; + addErrorMessage(lineNum, charNum, KEY, MSG); + } - const(Token)[] tokens = fce.unaryExpression - ? fce.unaryExpression.tokens - : fce.type - ? fce.type.tokens - : fce.tokens; + super.visit(statement); + } + } - addErrorMessage(tokens, KEY, MSG); - } + private bool hasUnusedResult(AST.Statement statement) + { + import dmd.astenums : TY; + + auto exprStatement = statement.isExpStatement(); + if (exprStatement is null) + return false; + + auto callExpr = exprStatement.exp.isCallExp(); + if (callExpr is null) + return false; + + string funcName = ""; + + if (auto identExpr = callExpr.e1.isIdentifierExp()) + funcName = cast(string) identExpr.ident.toString(); + else if (auto dotIdExpr = callExpr.e1.isDotIdExp()) + funcName = buildFullyQualifiedCallName(dotIdExpr); + + return (funcName in nonVoidFuncs) !is null; + } + + private extern (D) string buildFullyQualifiedCallName(AST.DotIdExp dotIdExpr) + { + import std.algorithm : fold, reverse; + + string[] nameStack; + nameStack ~= cast(string) dotIdExpr.ident.toString(); + + auto lastExpr = dotIdExpr.e1; + while (lastExpr.isDotIdExp()) + { + auto current = lastExpr.isDotIdExp(); + nameStack ~= cast(string) current.ident.toString(); + lastExpr = current.e1; + } + + if (auto identExpr = lastExpr.isIdentifierExp()) + nameStack ~= cast(string) identExpr.ident.toString(); + + return nameStack.reverse.fold!((a, b) => a ~ "." ~ b); + } } unittest { - import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; - import std.stdio : stderr; - import std.format : format; + import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; + import std.stdio : stderr; + import std.format : format; - StaticAnalysisConfig sac = disabledConfig(); - sac.unused_result = Check.enabled; + enum string MSG = "Function return value is discarded"; + StaticAnalysisConfig sac = disabledConfig(); + sac.unused_result = Check.enabled; - assertAnalyzerWarnings(q{ - void fun() {} + assertAnalyzerWarningsDMD(q{ + void fun() {} void main() { fun(); } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ alias noreturn = typeof(*null); noreturn fun() { while (1) {} } noreturn main() @@ -131,16 +230,15 @@ unittest } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ int fun() { return 1; } void main() { - fun(); /+ - ^^^ [warn]: %s +/ + fun(); // [warn]: %s } - }c.format(UnusedResultChecker.MSG), sac); + }c.format(MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct Foo { static bool get() @@ -151,12 +249,12 @@ unittest alias Bar = Foo; void main() { - Bar.get(); /+ - ^^^^^^^ [warn]: %s +/ + Bar.get(); // [warn]: %s + Foo.bar.get(); } - }c.format(UnusedResultChecker.MSG), sac); + }c.format(MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void main() { void fun() {} @@ -164,17 +262,15 @@ unittest } }c, sac); - version (none) // TODO: local functions - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void main() { int fun() { return 1; } - fun(); /+ - ^^^ [warn]: %s +/ + fun(); // [warn]: %s } - }c.format(UnusedResultChecker.MSG), sac); + }c.format(MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ int fun() { return 1; } void main() { @@ -182,7 +278,7 @@ unittest } }c, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ void fun() { } alias gun = fun; void main() @@ -191,7 +287,42 @@ unittest } }c, sac); - import std.stdio: writeln; - writeln("Unittest for UnusedResultChecker passed"); -} + assertAnalyzerWarningsDMD(q{ + int fun() { return 1; } + void main() + { + if (true) + fun(); // [warn]: %s + else + fun(); // [warn]: %s + } + }c.format(MSG, MSG), sac); + + assertAnalyzerWarningsDMD(q{ + int fun() { return 1; } + void main() + { + while (true) + fun(); // [warn]: %s + } + }c.format(MSG), sac); + assertAnalyzerWarningsDMD(q{ + int fun() { return 1; } + alias gun = fun; + void main() + { + gun(); // [warn]: %s + } + }c.format(MSG), sac); + + assertAnalyzerWarningsDMD(q{ + void main() + { + void fun() {} + fun(); + } + }c, sac); + + stderr.writeln("Unittest for UnusedResultChecker passed"); +} diff --git a/src/dscanner/analysis/unused_variable.d b/src/dscanner/analysis/unused_variable.d index 5b447e4a..c136179b 100644 --- a/src/dscanner/analysis/unused_variable.d +++ b/src/dscanner/analysis/unused_variable.d @@ -4,42 +4,269 @@ // http://www.boost.org/LICENSE_1_0.txt) module dscanner.analysis.unused_variable; -import dparse.ast; import dscanner.analysis.base; -import dscanner.analysis.unused; -import dsymbol.scope_ : Scope; -import std.algorithm.iteration : map; +import std.algorithm : all, canFind, each, endsWith, filter, map; /** * Checks for unused variables. */ -final class UnusedVariableCheck : UnusedStorageCheck +// TODO: many similarities to unused_param.d, maybe refactor into a common base class +extern (C++) class UnusedVariableCheck(AST) : BaseAnalyzerDmd { - alias visit = UnusedStorageCheck.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"unused_variable_check"; - /** - * Params: - * fileName = the name of the file being analyzed - */ - this(BaseAnalyzerArguments args) + private enum KEY = "dscanner.suspicious.unused_variable"; + private enum MSG = "Variable %s is never used."; + + private static struct VarInfo + { + string name; + ulong lineNum; + ulong charNum; + bool isUsed = false; + } + + private alias VarSet = VarInfo[string]; + private VarSet[] usedVars; + private bool inMixin; + private bool shouldIgnoreDecls; + private bool inFunction; + private bool inAggregate; + private bool shouldNotPushScope; + + extern (D) this(string fileName, bool skipTests = false) + { + super(fileName, skipTests); + pushScope(); + } + + override void visit(AST.FuncDeclaration funcDeclaration) + { + auto oldInFunction = inFunction; + inFunction = true; + super.visit(funcDeclaration); + inFunction = oldInFunction; + } + + mixin VisitAggregate!(AST.ClassDeclaration); + mixin VisitAggregate!(AST.StructDeclaration); + mixin VisitAggregate!(AST.UnionDeclaration); + + private template VisitAggregate(NodeType) + { + override void visit(NodeType node) + { + auto oldInFunction = inFunction; + auto oldInAggregate = inAggregate; + inFunction = false; + inAggregate = true; + super.visit(node); + inFunction = oldInFunction; + inAggregate = oldInAggregate; + } + } + + mixin VisitConditional!(AST.ConditionalDeclaration); + mixin VisitConditional!(AST.ConditionalStatement); + + private template VisitConditional(NodeType) + { + override void visit(NodeType node) + { + auto oldShouldNotPushScope = shouldNotPushScope; + shouldNotPushScope = true; + super.visit(node); + shouldNotPushScope = oldShouldNotPushScope; + } + } + + override void visit(AST.CompoundStatement compoundStatement) + { + if (!shouldNotPushScope) + pushScope(); + + super.visit(compoundStatement); + + if (!shouldNotPushScope) + popScope(); + } + + override void visit(AST.VarDeclaration varDeclaration) + { + super.visit(varDeclaration); + + if (varDeclaration.ident) + { + string varName = cast(string) varDeclaration.ident.toString(); + bool isAggregateField = inAggregate && !inFunction; + bool ignore = isAggregateField || shouldIgnoreDecls || varName.all!(c => c == '_'); + currentScope[varName] = VarInfo(varName, varDeclaration.loc.linnum, varDeclaration.loc.charnum, ignore); + } + } + + override void visit(AST.TypeAArray typeAArray) + { + import std.array : split; + + super.visit(typeAArray); + + string assocArrayStr = cast(string) typeAArray.toString(); + assocArrayStr.split('[') + .filter!(key => key.endsWith(']')) + .map!(key => key.split(']')[0]) + .each!(key => markAsUsed(key)); + } + + override void visit(AST.TemplateDeclaration templateDecl) { - super(args, "Variable", "unused_variable"); + super.visit(templateDecl); + + if (templateDecl.ident) + { + string varName = cast(string) templateDecl.ident.toString(); + bool isAggregateField = inAggregate && !inFunction; + bool ignore = isAggregateField || shouldIgnoreDecls || varName.all!(c => c == '_'); + currentScope[varName] = VarInfo(varName, templateDecl.loc.linnum, templateDecl.loc.charnum, ignore); + } } - override void visit(const VariableDeclaration variableDeclaration) + override void visit(AST.TypeSArray staticArray) { - foreach (d; variableDeclaration.declarators) - this.variableDeclared(d.name.text, d.name, false); - variableDeclaration.accept(this); + if (auto identifierExpression = staticArray.dim.isIdentifierExp()) + identifierExpression.accept(this); } - override void visit(const AutoDeclaration autoDeclaration) + override void visit(AST.IdentifierExp identifierExp) { - foreach (t; autoDeclaration.parts.map!(a => a.identifier)) - this.variableDeclared(t.text, t, false); - autoDeclaration.accept(this); + if (identifierExp.ident) + markAsUsed(cast(string) identifierExp.ident.toString()); + + super.visit(identifierExp); + } + + mixin VisitMixin!(AST.MixinExp); + mixin VisitMixin!(AST.MixinStatement); + + private template VisitMixin(NodeType) + { + override void visit(NodeType node) + { + inMixin = true; + super.visit(node); + inMixin = false; + } + } + + override void visit(AST.StringExp stringExp) + { + if (!inMixin) + return; + + string str = cast(string) stringExp.toStringz(); + currentScope.byKey + .filter!(param => canFind(str, param)) + .each!(param => markAsUsed(param)); + } + + override void visit(AST.TraitsExp traitsExp) + { + import dmd.dtemplate : isType; + + auto oldShouldIgnoreDecls = shouldIgnoreDecls; + + if (cast(string) traitsExp.ident.toString() == "compiles") + shouldIgnoreDecls = true; + + super.visit(traitsExp); + shouldIgnoreDecls = oldShouldIgnoreDecls; + + if (traitsExp.args is null) + return; + + (*traitsExp.args).opSlice().map!(arg => isType(arg)) + .filter!(type => type !is null) + .map!(type => type.isTypeIdentifier()) + .filter!(typeIdentifier => typeIdentifier !is null) + .each!(typeIdentifier => markAsUsed(cast(string) typeIdentifier.toString())); + } + + override void visit(AST.TypeTypeof typeOf) + { + auto oldShouldIgnoreDecls = shouldIgnoreDecls; + shouldIgnoreDecls = true; + super.visit(typeOf); + shouldIgnoreDecls = oldShouldIgnoreDecls; + } + + override void visit(AST.TemplateInstance templateInstance) + { + import dmd.dtemplate : isExpression, isType; + import dmd.mtype : Type; + + super.visit(templateInstance); + + if (templateInstance.name) + markAsUsed(cast(string) templateInstance.name.toString()); + + if (templateInstance.tiargs is null) + return; + + auto argSlice = (*templateInstance.tiargs).opSlice(); + + argSlice.map!(arg => arg.isExpression()) + .filter!(arg => arg !is null) + .map!(arg => arg.isIdentifierExp()) + .filter!(identifierExp => identifierExp !is null) + .each!(identifierExp => markAsUsed(cast(string) identifierExp.ident.toString())); + + argSlice.map!(arg => arg.isType()) + .filter!(arg => arg !is null) + .map!(arg => arg.isTypeIdentifier()) + .filter!(identifierType => identifierType !is null) + .each!(identifierType => markAsUsed(cast(string) identifierType.ident.toString())); + } + + private extern (D) void markAsUsed(string varName) + { + import std.range : retro; + + foreach (funcScope; usedVars.retro()) + { + if (varName in funcScope) + { + funcScope[varName].isUsed = true; + break; + } + } + } + + @property private extern (D) VarSet currentScope() + { + return usedVars[$ - 1]; + } + + private void pushScope() + { + // Error with gdc-12 + //usedVars ~= new VarSet; + + // Workaround for gdc-12 + VarSet newScope; + newScope["test"] = VarInfo("test", 0, 0); + usedVars ~= newScope; + currentScope.remove("test"); + } + + private void popScope() + { + import std.format : format; + + currentScope.byValue + .filter!(var => !var.isUsed) + .each!(var => addErrorMessage(var.lineNum, var.charNum, KEY, MSG.format(var.name))); + + usedVars.length--; } } @@ -47,11 +274,12 @@ final class UnusedVariableCheck : UnusedStorageCheck { import std.stdio : stderr; import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; StaticAnalysisConfig sac = disabledConfig(); sac.unused_variable_check = Check.enabled; - assertAnalyzerWarnings(q{ + + assertAnalyzerWarningsDMD(q{ // Issue 274 unittest @@ -62,8 +290,7 @@ final class UnusedVariableCheck : UnusedStorageCheck unittest { - int a; /+ - ^ [warn]: Variable a is never used. +/ + int a; // [warn]: Variable a is never used. } // Issue 380 @@ -76,8 +303,7 @@ final class UnusedVariableCheck : UnusedStorageCheck // Issue 380 int otherTemplatedEnum() { - auto a(T) = T.init; /+ - ^ [warn]: Variable a is never used. +/ + auto a(T) = T.init; // [warn]: Variable a is never used. return 0; } @@ -132,5 +358,26 @@ final class UnusedVariableCheck : UnusedStorageCheck } }c, sac); + + assertAnalyzerWarningsDMD(q{ + void testMixinExpression() + { + int a; + mixin("a = 5"); + } + }c, sac); + + assertAnalyzerWarningsDMD(q{ + bool f() + { + static if (is(S == bool) && is(typeof({ T s = "string"; }))) + { + return src ? "true" : "false"; + } + + return false; + } + }c, sac); + stderr.writeln("Unittest for UnusedVariableCheck passed."); } diff --git a/src/dscanner/analysis/useless_assert.d b/src/dscanner/analysis/useless_assert.d index 92072e80..efe945fa 100644 --- a/src/dscanner/analysis/useless_assert.d +++ b/src/dscanner/analysis/useless_assert.d @@ -7,94 +7,35 @@ module dscanner.analysis.useless_assert; import dscanner.analysis.base; import dscanner.analysis.helpers; -import dparse.ast; -import dparse.lexer; import std.stdio; -auto filterChars(string chars, S)(S str) -{ - import std.algorithm.comparison : among; - import std.algorithm.iteration : filter; - import std.meta : aliasSeqOf; - return str.filter!(c => !c.among(aliasSeqOf!chars)); -} - /** * Checks for asserts that always succeed */ -final class UselessAssertCheck : BaseAnalyzer +extern(C++) class UselessAssertCheck(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"useless_assert_check"; /// - this(BaseAnalyzerArguments args) + extern(D) this(string fileName, bool skipTests = false) { - super(args); + super(fileName, skipTests); } - override void visit(const AssertExpression ae) + override void visit(AST.AssertExp ae) { - import std.conv : to; + auto ie = ae.e1.isIntegerExp(); + if (ie && ie.getInteger() != 0) + addErrorMessage(cast(ulong) ae.loc.linnum, cast(ulong) ae.loc.charnum, KEY, MESSAGE); - UnaryExpression unary = cast(UnaryExpression) ae.assertArguments.assertion; - if (unary is null) - return; - if (unary.primaryExpression is null) - return; - immutable token = unary.primaryExpression.primary; - immutable skipSwitch = unary.primaryExpression.arrayLiteral !is null - || unary.primaryExpression.assocArrayLiteral !is null - || unary.primaryExpression.functionLiteralExpression !is null; - if (!skipSwitch) switch (token.type) - { - case tok!"doubleLiteral": - if (!token.text.filterChars!"Ll".to!double) - return; - break; - case tok!"floatLiteral": - if (!token.text.filterChars!"Ff".to!float) - return; - break; - case tok!"idoubleLiteral": - case tok!"ifloatLiteral": - case tok!"irealLiteral": - return; // `to` doesn't support imaginary numbers - case tok!"intLiteral": - if (!token.text.to!int) - return; - break; - case tok!"longLiteral": - if (!token.text.filterChars!"Ll".to!long) - return; - break; - case tok!"realLiteral": - if (!token.text.to!real) - return; - break; - case tok!"uintLiteral": - if (!token.text.filterChars!"Uu".to!uint) - return; - break; - case tok!"ulongLiteral": - if (!token.text.filterChars!"UuLl".to!ulong) - return; - break; - case tok!"characterLiteral": - if (token.text == `'\0'`) - return; - break; - case tok!"dstringLiteral": - case tok!"stringLiteral": - case tok!"wstringLiteral": - case tok!"true": - break; - default: - return; - } - addErrorMessage(unary, KEY, MESSAGE); + auto re = ae.e1.isRealExp(); + if (re && re.value != 0) + addErrorMessage(cast(ulong) ae.loc.linnum, cast(ulong) ae.loc.charnum, KEY, MESSAGE); + + if (ae.e1.isStringExp() || ae.e1.isArrayLiteralExp() || ae.e1.isAssocArrayLiteralExp()) + addErrorMessage(cast(ulong) ae.loc.linnum, cast(ulong) ae.loc.charnum, KEY, MESSAGE); } private: @@ -108,23 +49,20 @@ unittest import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; import std.format : format; + alias assertAnalyzerWarnings = assertAnalyzerWarningsDMD; + StaticAnalysisConfig sac = disabledConfig(); sac.useless_assert_check = Check.enabled; assertAnalyzerWarnings(q{ unittest { - assert(true); /+ - ^^^^ [warn]: %1$s +/ - assert(1); /+ - ^ [warn]: %1$s +/ - assert([10]); /+ - ^^^^ [warn]: %1$s +/ + assert(true); // [warn]: Assert condition is always true. + assert(1); // [warn]: Assert condition is always true. + assert([10]); // [warn]: Assert condition is always true. assert(false); assert(0); assert(0.0L); } - -}c - .format(UselessAssertCheck.MESSAGE), sac); +}c, sac); stderr.writeln("Unittest for UselessAssertCheck passed."); } diff --git a/src/dscanner/analysis/useless_initializer.d b/src/dscanner/analysis/useless_initializer.d index f2867d19..ce989c39 100644 --- a/src/dscanner/analysis/useless_initializer.d +++ b/src/dscanner/analysis/useless_initializer.d @@ -5,15 +5,8 @@ module dscanner.analysis.useless_initializer; import dscanner.analysis.base; -import dscanner.analysis.nolint; -import dscanner.utils : safeAccess; -import containers.dynamicarray; -import containers.hashmap; -import dparse.ast; -import dparse.lexer; -import std.algorithm; -import std.range : empty; -import std.stdio; +import dmd.astenums : InitKind, STC, TY; +import std.format : format; /* Limitations: @@ -26,301 +19,259 @@ Limitations: * Check that detects the initializers that are * not different from the implcit initializer. */ -final class UselessInitializerChecker : BaseAnalyzer +extern (C++) class UselessInitializerChecker(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"useless_initializer"; -private: - - enum string KEY = "dscanner.useless-initializer"; + private enum KEY = "dscanner.useless-initializer"; + private enum MSG = "Variable '%s' initializer is useless because it does not differ from the default value"; - version(unittest) + private struct StructInfo { - enum msg = "X"; + string name; + bool shouldErrorOnInit; + bool isBeingVisited; } - else + + private StructInfo[string] visitedStructs; + private string[] structStack; + private bool inTest; + + extern (D) this(string fileName, bool skipTests = false) { - enum msg = "Variable `%s` initializer is useless because it does not differ from the default value"; + super(fileName, skipTests); } - static immutable intDefs = ["0", "0L", "0UL", "0uL", "0U", "0x0", "0b0"]; - - HashMap!(string, bool) _structCanBeInit; - DynamicArray!(string) _structStack; - DynamicArray!(bool) _inStruct; - DynamicArray!(bool) _atDisabled; - bool _inTest; + override void visit(AST.UserAttributeDeclaration userAttribute) + { + if (shouldIgnoreDecl(userAttribute, KEY)) + return; -public: + super.visit(userAttribute); + } - /// - this(BaseAnalyzerArguments args) + override void visit(AST.Module mod) { - super(args); - _inStruct.insert(false); + if (shouldIgnoreDecl(mod.userAttribDecl(), KEY)) + return; + + super.visit(mod); } - override void visit(const(Unittest) test) + override void visit(AST.UnitTestDeclaration unitTestDecl) { if (skipTests) return; - _inTest = true; - test.accept(this); - _inTest = false; + + inTest = true; + super.visit(unitTestDecl); + inTest = false; } - override void visit(const(StructDeclaration) decl) + override void visit(AST.StructDeclaration structDecl) { - if (_inTest) + if (inTest || structDecl.ident is null) return; - assert(_inStruct.length > 1); + string structName = cast(string) structDecl.ident.toString(); + if (isNestedStruct()) + structName = structStack[$ - 1] ~ "." ~ structName; - const string structName = _inStruct[$-2] ? - _structStack.back() ~ "." ~ decl.name.text : - decl.name.text; + bool isDisabled = (structDecl.storage_class & STC.disable) != 0; + visitedStructs[structName] = StructInfo(structName, !isDisabled, true); + structStack ~= structName; + super.visit(structDecl); - _structStack.insert(structName); - _structCanBeInit[structName] = false; - _atDisabled.insert(false); - decl.accept(this); - _structStack.removeBack(); - _atDisabled.removeBack(); + visitedStructs[structName].isBeingVisited = false; + structStack.length--; } - override void visit(const(Declaration) decl) + private bool isNestedStruct() { - _inStruct.insert(decl.structDeclaration !is null); - - with (noLint.push(NoLintFactory.fromDeclaration(decl))) - decl.accept(this); + if (structStack.length >= 1) + return visitedStructs[structStack[$ - 1]].isBeingVisited; - if (_inStruct.length > 1 && _inStruct[$-2] && decl.constructor && - ((decl.constructor.parameters && decl.constructor.parameters.parameters.length == 0) || - !decl.constructor.parameters)) - { - _atDisabled[$-1] = decl.attributes - .canFind!(a => a.atAttribute !is null && a.atAttribute.identifier.text == "disable"); - } - _inStruct.removeBack(); + return false; } - override void visit(const(Constructor) decl) + override void visit(AST.CtorDeclaration ctorDeclaration) { - if (_inStruct.length > 1 && _inStruct[$-2] && - ((decl.parameters && decl.parameters.parameters.length == 0) || !decl.parameters)) - { - const bool canBeInit = !_atDisabled[$-1]; - _structCanBeInit[_structStack.back()] = canBeInit; - if (!canBeInit) - _structCanBeInit[_structStack.back()] = !decl.memberFunctionAttributes - .canFind!(a => a.atAttribute !is null && a.atAttribute.identifier.text == "disable"); - } - decl.accept(this); - } + super.visit(ctorDeclaration); - // issue 473, prevent to visit delegates that contain duck type checkers. - override void visit(const(TypeofExpression)) {} + bool isDefaultCtor = ctorDeclaration.getParameterList().length() == 0; - // issue 473, prevent to check expressions in __traits(compiles, ...) - override void visit(const(TraitsExpression) e) + if (structStack.length == 0 || !isDefaultCtor) + return; + + auto structName = structStack[$ - 1]; + if (!visitedStructs[structName].isBeingVisited || !visitedStructs[structName].shouldErrorOnInit) + return; + + bool isDisabled = (ctorDeclaration.storage_class & STC.disable) != 0; + visitedStructs[structName].shouldErrorOnInit = !isDisabled; + } + + override void visit(AST.VarDeclaration varDecl) { - if (e.identifier.text == "compiles") - { + import std.format : format; + + super.visit(varDecl); + + // issue 474, manifest constants HAVE to be initialized initializer has to appear clearly in generated ddoc + // https://github.com/dlang-community/d-Scanner/issues/474 + if (varDecl._init is null || varDecl.storage_class & STC.manifest || varDecl.comment()) return; + + ulong lineNum = cast(ulong) varDecl.loc.linnum; + ulong charNum = cast(ulong) varDecl.loc.charnum; + string msg = MSG.format(varDecl.ident.toString()); + + if (auto expInit = varDecl._init.isExpInitializer()) + { + bool isBasicType; + if (varDecl.type) + isBasicType = isBasicTypeConstant(varDecl.type.ty); + + if (isRedundantExpInit(expInit.exp, isBasicType)) + addErrorMessage(lineNum, charNum, KEY, msg); } - else + else if (auto arrInit = varDecl._init.isArrayInitializer()) { - e.accept(this); + if (arrInit.dim == 0 && arrInit.index.length == 0 && arrInit.value.length == 0) + addErrorMessage(lineNum, charNum, KEY, msg); } } - override void visit(const(VariableDeclaration) decl) + private bool isBasicTypeConstant(TY type) { - if (!decl.type || !decl.type.type2 || - // initializer has to appear clearly in generated ddoc - decl.comment !is null || - // issue 474, manifest constants HAVE to be initialized. - decl.storageClasses.canFind!(a => a.token == tok!"enum")) - { - return; - } + return (type >= TY.Tint8 && type <= TY.Tdchar) || type == TY.Tint128 || type == TY.Tuns128; + } + + private bool isRedundantExpInit(AST.Expression exp, bool isBasicType) + { + if (auto intExp = exp.isIntegerExp()) + return intExp.getInteger() == 0; - foreach (declarator; decl.declarators) + if (auto dotIdExp = exp.isDotIdExp()) { - if (!declarator.initializer || - !declarator.initializer.nonVoidInitializer || - declarator.comment !is null) - { - continue; - } + if (dotIdExp.ident is null) + return false; + + bool shouldLookForInit; - version(unittest) + if (isBasicType) { - void warn(const BaseNode range) - { - addErrorMessage(range, KEY, msg); - } + shouldLookForInit = true; } else { - import std.format : format; - void warn(const BaseNode range) - { - addErrorMessage(range, KEY, msg.format(declarator.name.text)); - } + string structName = computeStructNameFromDotChain(dotIdExp); + if (structName in visitedStructs) + shouldLookForInit = visitedStructs[structName].shouldErrorOnInit; } - // --- Info about the declaration type --- // - const bool isPtr = decl.type.typeSuffixes && decl.type.typeSuffixes - .canFind!(a => a.star != tok!""); - const bool isArr = decl.type.typeSuffixes && decl.type.typeSuffixes - .canFind!(a => a.array); + if (shouldLookForInit) + return cast(string) dotIdExp.ident.toString() == "init"; - bool isStr, isSzInt; - Token customType; + return false; + } - if (const TypeIdentifierPart tip = safeAccess(decl).type.type2.typeIdentifierPart) - { - if (!tip.typeIdentifierPart) - { - customType = tip.identifierOrTemplateInstance.identifier; - isStr = customType.text.among("string", "wstring", "dstring") != 0; - isSzInt = customType.text.among("size_t", "ptrdiff_t") != 0; - } - } + return exp.isNullExp() !is null; + } - // --- 'BasicType/Symbol AssignExpression' ---// - const NonVoidInitializer nvi = declarator.initializer.nonVoidInitializer; - const UnaryExpression ue = cast(UnaryExpression) nvi.assignExpression; - if (ue && ue.primaryExpression) - { - const Token value = ue.primaryExpression.primary; - - if (!isPtr && !isArr && !isStr && decl.type.type2.builtinType != tok!"") - { - switch(decl.type.type2.builtinType) - { - // check for common cases of default values - case tok!"byte", tok!"ubyte": - case tok!"short", tok!"ushort": - case tok!"int", tok!"uint": - case tok!"long", tok!"ulong": - case tok!"cent", tok!"ucent": - case tok!"bool": - if (intDefs.canFind(value.text) || value == tok!"false") - warn(nvi); - goto default; - default: - // check for BasicType.init - if (ue.primaryExpression.basicType.type == decl.type.type2.builtinType && - ue.primaryExpression.primary.text == "init" && - !ue.primaryExpression.expression) - warn(nvi); - } - } - else if (isSzInt) - { - if (intDefs.canFind(value.text)) - warn(nvi); - } - else if (isPtr || isStr) - { - if (str(value.type) == "null") - warn(nvi); - } - else if (isArr) - { - if (str(value.type) == "null") - warn(nvi); - else if (nvi.arrayInitializer && nvi.arrayInitializer.arrayMemberInitializations.length == 0) - warn(nvi); - } - } + private extern (D) string computeStructNameFromDotChain(AST.DotIdExp dotIdExp) + { + if (dotIdExp.ident is null) + return ""; - else if (const IdentifierOrTemplateInstance iot = safeAccess(ue) - .unaryExpression.primaryExpression.identifierOrTemplateInstance) - { - // Symbol s = Symbol.init - if (ue && customType != tok!"" && iot.identifier == customType && - ue.identifierOrTemplateInstance && ue.identifierOrTemplateInstance.identifier.text == "init") - { - if (customType.text in _structCanBeInit) - { - if (!_structCanBeInit[customType.text]) - warn(nvi); - } - } - } + string name; + auto parent = dotIdExp.e1; - // 'Symbol ArrayInitializer' : assumes Symbol is an array b/c of the Init - else if (nvi.arrayInitializer && (isArr || isStr)) - { - if (nvi.arrayInitializer.arrayMemberInitializations.length == 0) - warn(nvi); - } + while (parent && parent.isDotIdExp()) + { + auto dotIdParent = parent.isDotIdExp(); + if (dotIdParent.ident is null) + return ""; + + name = cast(string) dotIdParent.ident.toString() ~ "." ~ name; + parent = dotIdParent.e1; } - decl.accept(this); + auto idExp = parent.isIdentifierExp(); + if (idExp && idExp.ident) + { + string structName = cast(string) idExp.ident.toString(); + if (name.length > 0) + return structName = structName ~ "." ~ name[0 .. $ - 1]; + + return structName; + } + + return ""; + } + + // issue 473, prevent to visit delegates that contain duck type checkers. + // https://github.com/dlang-community/d-Scanner/issues/473 + override void visit(AST.TypeTypeof _) + { + } + + // issue 473, prevent to check expressions in __traits(compiles, ...) + // https://github.com/dlang-community/d-Scanner/issues/473 + override void visit(AST.TraitsExp traitsExp) + { + if (traitsExp.ident.toString() != "compiles") + super.visit(traitsExp); } } @system unittest { import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig; - import dscanner.analysis.helpers: assertAnalyzerWarnings; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig; sac.useless_initializer = Check.enabled; + enum msgA = "Variable 'a' initializer is useless because it does not differ from the default value"; + enum msgS = "Variable 's' initializer is useless because it does not differ from the default value"; + + assertAnalyzerWarningsDMD(q{ + struct Outer + { + struct Inner {} + } + Outer.Inner s = Outer.Inner.init; // [warn]: %s + }c.format(msgS), sac); // fails - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct S {} - ubyte a = 0x0; /+ - ^^^ [warn]: X +/ - int a = 0; /+ - ^ [warn]: X +/ - ulong a = 0; /+ - ^ [warn]: X +/ - int* a = null; /+ - ^^^^ [warn]: X +/ - Foo* a = null; /+ - ^^^^ [warn]: X +/ - int[] a = null; /+ - ^^^^ [warn]: X +/ - int[] a = []; /+ - ^^ [warn]: X +/ - string a = null; /+ - ^^^^ [warn]: X +/ - string a = null; /+ - ^^^^ [warn]: X +/ - wstring a = null; /+ - ^^^^ [warn]: X +/ - dstring a = null; /+ - ^^^^ [warn]: X +/ - size_t a = 0; /+ - ^ [warn]: X +/ - ptrdiff_t a = 0; /+ - ^ [warn]: X +/ - string a = []; /+ - ^^ [warn]: X +/ - char[] a = null; /+ - ^^^^ [warn]: X +/ - int a = int.init; /+ - ^^^^^^^^ [warn]: X +/ - char a = char.init; /+ - ^^^^^^^^^ [warn]: X +/ - S s = S.init; /+ - ^^^^^^ [warn]: X +/ - bool a = false; /+ - ^^^^^ [warn]: X +/ - }, sac); + ubyte a = 0x0; // [warn]: %s + int a = 0; // [warn]: %s + ulong a = 0; // [warn]: %s + int* a = null; // [warn]: %s + Foo* a = null; // [warn]: %s + int[] a = null; // [warn]: %s + int[] a = []; // [warn]: %s + string a = null; // [warn]: %s + string a = null; // [warn]: %s + wstring a = null; // [warn]: %s + dstring a = null; // [warn]: %s + size_t a = 0; // [warn]: %s + ptrdiff_t a = 0; // [warn]: %s + string a = []; // [warn]: %s + char[] a = null; // [warn]: %s + int a = int.init; // [warn]: %s + char a = char.init; // [warn]: %s + S s = S.init; // [warn]: %s + bool a = false; // [warn]: %s + }.format(msgA, msgA, msgA, msgA, msgA, msgA, msgA, msgA, msgA, msgA, msgA, + msgA, msgA, msgA, msgA, msgA, msgA, msgS, msgA), sac); // passes - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ struct D {@disable this();} struct E {this() @disable;} ubyte a = 0xFE; @@ -357,6 +308,7 @@ public: S s = s.call(); enum {a} enum ubyte a = 0; + int a = 0; /// Documented with default initializer static assert(is(typeof((){T t = T.init;}))); void foo(){__traits(compiles, (){int a = 0;}).writeln;} bool a; @@ -366,11 +318,10 @@ public: }, sac); // passes - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ @("nolint(dscanner.useless-initializer)") - int a = 0; - int a = 0; /+ - ^ [warn]: X +/ + int x = 0; + int a = 0; // [warn]: %s @("nolint(dscanner.useless-initializer)") int f() { @@ -379,21 +330,20 @@ public: struct nolint { string s; } - @nolint("dscanner.useless-initializer") - int a = 0; - int a = 0; /+ - ^ [warn]: X +/ + @("nolint(dscanner.useless-initializer)") + int c = 0; + int s = 0; // [warn]: %s @("nolint(other_check, dscanner.useless-initializer, another_one)") - int a = 0; + int e = 0; - @nolint("other_check", "another_one", "dscanner.useless-initializer") - int a = 0; + @("nolint(other_check, another_one, dscanner.useless-initializer)") + int f = 0; - }, sac); + }c.format(msgA, msgS), sac); // passes (disable check at module level) - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ @("nolint(dscanner.useless-initializer)") module my_module; @@ -402,8 +352,7 @@ public: int f() { int a = 0; } - }, sac); + }c, sac); stderr.writeln("Unittest for UselessInitializerChecker passed."); } - diff --git a/src/dscanner/analysis/vcall_in_ctor.d b/src/dscanner/analysis/vcall_in_ctor.d index 93c68017..5f536d31 100644 --- a/src/dscanner/analysis/vcall_in_ctor.d +++ b/src/dscanner/analysis/vcall_in_ctor.d @@ -5,10 +5,7 @@ module dscanner.analysis.vcall_in_ctor; import dscanner.analysis.base; -import dscanner.utils; -import dparse.ast, dparse.lexer; -import std.algorithm.searching : canFind; -import std.range: retro; +import dmd.astenums : STC; /** * Checks virtual calls from the constructor to methods defined in the same class. @@ -16,337 +13,225 @@ import std.range: retro; * When not used carefully, virtual calls from constructors can lead to a call * in a derived instance that's not yet constructed. */ -final class VcallCtorChecker : BaseAnalyzer +extern (C++) class VcallCtorChecker(AST) : BaseAnalyzerDmd { - alias visit = BaseAnalyzer.visit; - + alias visit = BaseAnalyzerDmd.visit; mixin AnalyzerInfo!"vcall_in_ctor"; -private: - - enum string KEY = "dscanner.vcall_ctor"; - enum string MSG = "a virtual call inside a constructor may lead to" - ~ " unexpected results in the derived classes"; - - // what's called in the ctor - Token[][] _ctorCalls; - // the virtual method in the classes - Token[][] _virtualMethods; + private enum string KEY = "dscanner.vcall_ctor"; + private enum string MSG = "a virtual call inside a constructor may lead to unexpected results in the derived classes"; - - // The problem only happens in classes - bool[] _inClass = [false]; - // The problem only happens in __ctor - bool[] _inCtor = [false]; - // The problem only happens with call to virtual methods - bool[] _isVirtual = [true]; - // The problem only happens with call to virtual methods - bool[] _isNestedFun = [false]; - // The problem only happens in derived classes that override - bool[] _isFinal = [false]; - - void pushVirtual(bool value) + private static struct FuncContext { - _isVirtual ~= value; + bool canBeVirtual; + bool hasNonVirtualVis; + bool hasNonVirtualStg; + bool inCtor; } - void pushInClass(bool value) + private static struct CallContext { - _inClass ~= value; - _ctorCalls.length += 1; - _virtualMethods.length += 1; + string funcName; + ulong lineNum; + ulong charNum; } - void pushInCtor(bool value) - { - _inCtor ~= value; - } + private FuncContext[] contexts; + private bool[string] virtualFuncs; + private CallContext[] ctorCalls; + private bool isFinal; - void pushNestedFunc(bool value) + extern (D) this(string filename, bool skipTests = false) { - _isNestedFun ~= value; + super(filename, skipTests); } - void pushIsFinal(bool value) + override void visit(AST.ClassDeclaration classDecl) { - _isFinal ~= value; + pushContext((classDecl.storage_class & STC.final_) == 0 && !isFinal); + super.visit(classDecl); + checkForVirtualCalls(); + popContext(); } - void popVirtual() + override void visit(AST.StructDeclaration structDecl) { - _isVirtual.length -= 1; + pushContext(false); + super.visit(structDecl); + checkForVirtualCalls(); + popContext(); } - void popInClass() + private void checkForVirtualCalls() { - _inClass.length -= 1; - _ctorCalls.length -= 1; - _virtualMethods.length -= 1; - } + import std.algorithm : each, filter; - void popInCtor() - { - _inCtor.length -= 1; + ctorCalls.filter!(call => call.funcName in virtualFuncs) + .each!(call => addErrorMessage(call.lineNum, call.charNum, KEY, MSG)); } - void popNestedFunc() + override void visit(AST.VisibilityDeclaration visDecl) { - _isNestedFun.length -= 1; - } + import dmd.dsymbol : Visibility; - void popIsFinal() - { - _isFinal.length -= 1; - } - - void overwriteVirtual(bool value) - { - _isVirtual[$-1] = value; - } + if (contexts.length == 0) + { + super.visit(visDecl); + return; + } - bool isVirtual() - { - return _isVirtual[$-1]; + bool oldVis = currentContext.hasNonVirtualVis; + currentContext.hasNonVirtualVis = visDecl.visibility.kind == Visibility.Kind.private_ + || visDecl.visibility.kind == Visibility.Kind.package_; + super.visit(visDecl); + currentContext.hasNonVirtualVis = oldVis; } - bool isInClass() + override void visit(AST.StorageClassDeclaration stgDecl) { - return _inClass[$-1]; - } + bool oldFinal = isFinal; + isFinal = (stgDecl.stc & STC.final_) != 0; - bool isInCtor() - { - return _inCtor[$-1]; - } + bool oldStg; + if (contexts.length > 0) + { + oldStg = currentContext.hasNonVirtualStg; + currentContext.hasNonVirtualStg = !(stgDecl.stc & STC.static_ || stgDecl.stc & STC.final_); + } - bool isFinal() - { - return _isFinal[$-1]; - } + super.visit(stgDecl); - bool isInNestedFunc() - { - return _isNestedFun[$-1]; + isFinal = oldFinal; + if (contexts.length > 0) + currentContext.hasNonVirtualStg = oldStg; } - void check() + override void visit(AST.FuncDeclaration funcDecl) { - foreach (call; _ctorCalls[$-1]) - foreach (vm; _virtualMethods[$-1]) + if (contexts.length == 0) { - if (call == vm) - { - addErrorMessage(call, KEY, MSG); - break; - } + super.visit(funcDecl); + return; } - } - -public: - /// - this(BaseAnalyzerArguments args) - { - super(args); - } + bool hasVirtualBody; + if (funcDecl.fbody !is null) + { + auto funcBody = funcDecl.fbody.isCompoundStatement(); + hasVirtualBody = funcBody !is null && funcBody.statements !is null && (*funcBody.statements).length == 0; + } + else + { + hasVirtualBody = true; + } - override void visit(const(ClassDeclaration) decl) - { - pushVirtual(true); - pushInClass(true); - pushNestedFunc(false); - decl.accept(this); - check(); - popVirtual(); - popInClass(); - popNestedFunc(); - } + bool hasNonVirtualStg = currentContext.hasNonVirtualStg + || funcDecl.storage_class & STC.static_ || funcDecl.storage_class & STC.final_; - override void visit(const(StructDeclaration) decl) - { - pushVirtual(false); - pushInClass(false); - pushNestedFunc(false); - decl.accept(this); - check(); - popVirtual(); - popInClass(); - popNestedFunc(); - } + if (!currentContext.canBeVirtual || currentContext.hasNonVirtualVis || hasNonVirtualStg || !hasVirtualBody) + { + super.visit(funcDecl); + return; + } - override void visit(const(Constructor) ctor) - { - pushInCtor(isInClass); - ctor.accept(this); - popInCtor(); + string funcName = cast(string) funcDecl.ident.toString(); + virtualFuncs[funcName] = true; } - override void visit(const(Declaration) d) + override void visit(AST.CtorDeclaration ctorDecl) { - // ":" - if (d.attributeDeclaration && d.attributeDeclaration.attribute) + if (contexts.length == 0) { - const tp = d.attributeDeclaration.attribute.attribute.type; - overwriteVirtual(isProtection(tp) & (tp != tok!"private")); + super.visit(ctorDecl); + return; } - // "protection {}" - bool pop; - scope(exit) if (pop) - popVirtual; - - const bool hasAttribs = d.attributes !is null; - const bool hasStatic = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"static") : false; - const bool hasFinal = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"final") : false; + currentContext.inCtor = true; + super.visit(ctorDecl); + currentContext.inCtor = false; + } - if (d.attributes) foreach (attr; d.attributes.retro) - { - if (!hasStatic && - (attr.attribute == tok!"public" || attr.attribute == tok!"protected")) - { - pushVirtual(true); - pop = true; - break; - } - else if (hasStatic || attr.attribute == tok!"private" || attr.attribute == tok!"package") - { - pushVirtual(false); - pop = true; - break; - } - } + override void visit(AST.CallExp callExp) + { + super.visit(callExp); - // final class... final function - if ((d.classDeclaration || d.functionDeclaration) && hasFinal) - pushIsFinal(true); + if (contexts.length == 0) + return; - d.accept(this); + auto identExp = callExp.e1.isIdentifierExp(); + if (!currentContext.inCtor || identExp is null) + return; - if ((d.classDeclaration || d.functionDeclaration) && hasFinal) - popIsFinal; + string funcCall = cast(string) identExp.ident.toString(); + ctorCalls ~= CallContext(funcCall, callExp.loc.linnum, callExp.loc.charnum); } - override void visit(const(FunctionCallExpression) exp) + private ref currentContext() @property { - // nested function are not virtual - pushNestedFunc(true); - exp.accept(this); - popNestedFunc(); + return contexts[$ - 1]; } - override void visit(const(UnaryExpression) exp) + private void pushContext(bool inClass) { - if (isInCtor) - // get function identifier for a call, only for this member (so no ident chain) - if (const IdentifierOrTemplateInstance iot = safeAccess(exp) - .functionCallExpression.unaryExpression.primaryExpression.identifierOrTemplateInstance) - { - const Token t = iot.identifier; - if (t != tok!"") - { - _ctorCalls[$-1] ~= t; - } - } - exp.accept(this); + contexts ~= FuncContext(inClass); } - override void visit(const(FunctionDeclaration) d) + private void popContext() { - if (isInClass() && !isInNestedFunc() && !isFinal() && !d.templateParameters) - { - bool virtualOnce; - bool notVirtualOnce; - - const bool hasAttribs = d.attributes !is null; - const bool hasStatic = hasAttribs ? d.attributes.canFind!(a => a.attribute.type == tok!"static") : false; - - // handle "private", "public"... for this declaration - if (d.attributes) foreach (attr; d.attributes.retro) - { - if (!hasStatic && - (attr.attribute == tok!"public" || attr.attribute == tok!"protected")) - { - if (!isVirtual) - { - virtualOnce = true; - break; - } - } - else if (hasStatic || attr.attribute == tok!"private" || attr.attribute == tok!"package") - { - if (isVirtual) - { - notVirtualOnce = true; - break; - } - } - } - - if (!isVirtual && virtualOnce) - _virtualMethods[$-1] ~= d.name; - else if (isVirtual && !virtualOnce) - _virtualMethods[$-1] ~= d.name; - - } - d.accept(this); + contexts.length--; } } unittest { import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; - import dscanner.analysis.helpers : assertAnalyzerWarnings; - import std.stdio : stderr; + import dscanner.analysis.helpers : assertAnalyzerWarningsDMD; import std.format : format; + import std.stdio : stderr; StaticAnalysisConfig sac = disabledConfig(); sac.vcall_in_ctor = Check.enabled; + string MSG = "a virtual call inside a constructor may lead to unexpected results in the derived classes"; // fails - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { - this(){foo();} /+ - ^^^ [warn]: %s +/ + this(){foo();} // [warn]: %s private: - public - void foo(){} - + public void foo(){} } - }c.format(VcallCtorChecker.MSG), sac); + }c.format(MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { this() { - foo(); /+ - ^^^ [warn]: %s +/ - foo(); /+ - ^^^ [warn]: %s +/ + foo(); // [warn]: %s + foo(); // [warn]: %s bar(); } private: void bar(); public{void foo(){}} } - }c.format(VcallCtorChecker.MSG, VcallCtorChecker.MSG), sac); + }c.format(MSG, MSG), sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { this() { foo(); - bar(); /+ - ^^^ [warn]: %s +/ + bar(); // [warn]: %s } private: public void bar(); - public private {void foo(){}} + private {void foo(){}} } - }c.format(VcallCtorChecker.MSG), sac); + }c.format(MSG), sac); // passes - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { this(){foo();} @@ -354,7 +239,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { this(){foo();} @@ -362,23 +247,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ - class Bar - { - this(){foo();} - private public protected private void foo(){} - } - }, sac); - - assertAnalyzerWarnings(q{ - class Bar - { - this(){foo();} - final private public protected void foo(){} - } - }, sac); - - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { this(){foo();} @@ -386,7 +255,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ final class Bar { public: @@ -395,7 +264,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Bar { public: @@ -404,7 +273,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo { static void nonVirtual(); @@ -412,7 +281,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class Foo { package void nonVirtual(); @@ -420,7 +289,7 @@ unittest } }, sac); - assertAnalyzerWarnings(q{ + assertAnalyzerWarningsDMD(q{ class C { static struct S { public: @@ -432,7 +301,5 @@ unittest } }, sac); - import std.stdio: writeln; - writeln("Unittest for VcallCtorChecker passed"); + stderr.writeln("Unittest for VcallCtorChecker passed"); } - diff --git a/src/dscanner/imports.d b/src/dscanner/imports.d index b2b6fccc..657db4d8 100644 --- a/src/dscanner/imports.d +++ b/src/dscanner/imports.d @@ -5,75 +5,81 @@ module dscanner.imports; -import dparse.ast; -import dparse.lexer; -import dparse.parser; -import dparse.rollback_allocator; import std.stdio; import std.container.rbtree; import std.functional : toDelegate; import dscanner.utils; +import dmd.visitor.permissive; +import dmd.visitor.transitive; +import dmd.tokens; +import dmd.common.outbuffer; +import core.stdc.stdio; +import dmd.parse; +import dmd.astbase; +import dmd.id; +import dmd.globals; +import dmd.identifier; +import core.memory; +import std.stdio; +import std.file; +import std.conv : to; -/** - * AST visitor that collects modules imported to an R-B tree. - */ -class ImportPrinter : ASTVisitor +extern(C++) class ImportVisitor(AST) : ParseTimeTransitiveVisitor!AST { + alias visit = ParseTimeTransitiveVisitor!AST.visit; + this() { imports = new RedBlackTree!string; } - override void visit(const SingleImport singleImport) - { - ignore = false; - singleImport.accept(this); - ignore = true; - } - - override void visit(const IdentifierChain identifierChain) - { - if (ignore) - return; - bool first = true; + override void visit(AST.Import imp) + { + import std.conv : to; string s; - foreach (ident; identifierChain.identifiers) - { - if (!first) - s ~= "."; - s ~= ident.text; - first = false; - } - imports.insert(s); - } - alias visit = ASTVisitor.visit; + foreach (const pid; imp.packages) + s = s ~ to!string(pid.toChars()) ~ "."; - /// Collected imports - RedBlackTree!string imports; + s ~= to!string(imp.id.toChars()); + imports.insert(s); + } -private: - bool ignore = true; + RedBlackTree!string imports; } -private void visitFile(bool usingStdin, string fileName, RedBlackTree!string importedModules, StringCache* cache) +private void visitFile(bool usingStdin, string fileName, RedBlackTree!string importedModules) { - RollbackAllocator rba; - LexerConfig config; - config.fileName = fileName; - config.stringBehavior = StringBehavior.source; - auto visitor = new ImportPrinter; - auto tokens = getTokensForParser(usingStdin ? readStdin() : readFile(fileName), config, cache); - auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing)); - visitor.visit(mod); - importedModules.insert(visitor.imports[]); + import dmd.errorsink : ErrorSinkNull; + + Id.initialize(); + global._init(); + global.params.useUnitTests = true; + ASTBase.Type._init(); + + auto id = Identifier.idPool(fileName); + auto m = new ASTBase.Module(&(fileName.dup)[0], id, false, false); + ubyte[] bytes = usingStdin ? readStdin() : readFile(fileName); + auto input = cast(char[]) bytes; + + __gshared ErrorSinkNull errorSinkNull; + if (!errorSinkNull) + errorSinkNull = new ErrorSinkNull; + + scope p = new Parser!ASTBase(m, input, false, new ErrorSinkNull, null, false); + p.nextToken(); + m.members = p.parseModule(); + + scope vis = new ImportVisitor!ASTBase(); + m.accept(vis); + importedModules.insert(vis.imports[]); } private void doNothing(string, size_t, size_t, string, bool) { } -void printImports(bool usingStdin, string[] args, string[] importPaths, StringCache* cache, bool recursive) +void printImports(bool usingStdin, string[] args, string[] importPaths, bool recursive) { string[] fileNames = usingStdin ? ["stdin"] : expandArgs(args); import std.path : buildPath, dirSeparator; @@ -85,7 +91,7 @@ void printImports(bool usingStdin, string[] args, string[] importPaths, StringCa auto resolvedLocations = new RedBlackTree!(string); auto importedFiles = new RedBlackTree!(string); foreach (name; fileNames) - visitFile(usingStdin, name, importedFiles, cache); + visitFile(usingStdin, name, importedFiles); if (importPaths.empty) { foreach (item; importedFiles[]) @@ -110,7 +116,7 @@ void printImports(bool usingStdin, string[] args, string[] importPaths, StringCa resolvedModules.insert(item); resolvedLocations.insert(alt); if (recursive) - visitFile(false, alt, newlyDiscovered, cache); + visitFile(false, alt, newlyDiscovered); continue itemLoop; } } @@ -126,3 +132,37 @@ void printImports(bool usingStdin, string[] args, string[] importPaths, StringCa foreach (resolved; resolvedLocations[]) writeln(resolved); } + +unittest +{ + import std.stdio : File; + import std.file : exists, remove; + + auto deleteme = "test.txt"; + File file = File(deleteme, "w"); + scope(exit) + { + assert(exists(deleteme)); + remove(deleteme); + } + + file.write(q{ + import std.stdio; + import std.fish : scales, head; + import DAGRON = std.experimental.dragon; + import std.file; + }); + + file.close(); + + auto importedFiles = new RedBlackTree!(string); + visitFile(false, deleteme, importedFiles); + + auto expected = new RedBlackTree!(string); + expected.insert("std.stdio"); + expected.insert("std.fish"); + expected.insert("std.file"); + expected.insert("std.experimental.dragon"); + + assert(expected == importedFiles); +} diff --git a/src/dscanner/main.d b/src/dscanner/main.d index b501ae9c..70e90903 100644 --- a/src/dscanner/main.d +++ b/src/dscanner/main.d @@ -31,6 +31,7 @@ import dscanner.outliner; import dscanner.symbol_finder; import dscanner.analysis.run; import dscanner.analysis.config; +import dscanner.analysis.autofix : listAutofixes; import dscanner.dscanner_version; import dscanner.utils; @@ -140,7 +141,7 @@ else // users can use verbose to enable all logs (this will log things like // dsymbol couldn't find some modules due to wrong import paths) static if (__VERSION__ >= 2_101) - (cast()sharedLog).logLevel = verbose ? LogLevel.all : LogLevel.error; + (cast() sharedLog).logLevel = verbose ? LogLevel.all : LogLevel.error; else globalLogLevel = verbose ? LogLevel.all : LogLevel.error; } @@ -204,9 +205,9 @@ else if (excludePaths.length) { string[] newArgs = [expanded[0]]; - foreach(arg; args[1 .. $]) + foreach (arg; args[1 .. $]) { - if(!excludePaths.map!(p => arg.isSubpathOf(p)) + if (!excludePaths.map!(p => arg.isSubpathOf(p)) .fold!((a, b) => a || b)) newArgs ~= arg; } @@ -324,11 +325,11 @@ else if (autofix) { - return .autofix(expandedArgs, config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0; + return .autofix(expandedArgs, config, errorFormat, applySingleFixes) ? 1 : 0; } else if (resolveMessage.length) { - listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1], &cache, moduleCache); + listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1]); return 0; } else if (report) @@ -340,19 +341,19 @@ else goto case; case "": case "dscanner": - generateReport(expandedArgs, config, cache, moduleCache, reportFile); + generateReport(expandedArgs, config, reportFile); break; case "sonarQubeGenericIssueData": - generateSonarQubeGenericIssueDataReport(expandedArgs, config, cache, moduleCache, reportFile); + generateSonarQubeGenericIssueDataReport(expandedArgs, config, reportFile); break; } } else - return analyze(expandedArgs, config, errorFormat, cache, moduleCache, true) ? 1 : 0; + return analyze(expandedArgs, config, errorFormat) ? 1 : 0; } else if (syntaxCheck) { - return .syntaxCheck(usingStdin ? ["stdin"] : expandedArgs, errorFormat, cache, moduleCache) ? 1 : 0; + return .syntaxCheck(usingStdin ? ["stdin"] : expandedArgs, errorFormat) ? 1 : 0; } else { @@ -387,7 +388,7 @@ else } else if (imports || recursiveImports) { - printImports(usingStdin, args, importPaths, &cache, recursiveImports); + printImports(usingStdin, args, importPaths, recursiveImports); } else if (ast || outline) { @@ -599,7 +600,7 @@ string getDefaultConfigurationLocation() configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME); return configDir; } - else version(Windows) + else version (Windows) { string configDir = environment.get("APPDATA", null); enforce(configDir !is null, "%APPDATA% is unset"); diff --git a/src/dscanner/reports.d b/src/dscanner/reports.d index c35d9b03..0d5cc463 100644 --- a/src/dscanner/reports.d +++ b/src/dscanner/reports.d @@ -37,7 +37,7 @@ class DScannerJsonReporter _issues ~= toIssue(message, isError); } - string getContent(StatsCollector stats, ulong lineOfCodeCount) + string getContent(AST)(StatsCollector!AST stats, ulong lineOfCodeCount) { JSONValue result = [ "issues" : JSONValue(_issues.data.map!(e => toJson(e)).array), diff --git a/src/dscanner/utils.d b/src/dscanner/utils.d index 504432e4..47b1ebc9 100644 --- a/src/dscanner/utils.d +++ b/src/dscanner/utils.d @@ -8,6 +8,10 @@ import std.format : format; import std.file : exists, read; import std.path: isValidPath; +import dmd.astbase : ASTBase; +import dmd.parse : Parser; +import dmd.astcodegen; + private void processBOM(ref ubyte[] sourceCode, string fname) { enum spec = "D-Scanner does not support %s-encoded files (%s)"; @@ -309,3 +313,24 @@ auto ref safeAccess(M)(M m) { return SafeAccess!M(m); } + +/** + * Return the module name from a ModuleDeclaration instance with the following format: `foo.bar.module` + */ +const(char[]) getModuleName(ASTCodegen.ModuleDeclaration *mdptr) +{ + import std.array : array, join; + + if (mdptr !is null) + { + import std.algorithm : map; + ASTCodegen.ModuleDeclaration md = *mdptr; + + if (md.packages.length != 0) + return join(md.packages.map!(e => e.toString()).array ~ md.id.toString().dup, "."); + else + return md.id.toString(); + } + + return ""; +} \ No newline at end of file diff --git a/tests/it.sh b/tests/it.sh index b4990d14..5df4a57a 100755 --- a/tests/it.sh +++ b/tests/it.sh @@ -41,7 +41,12 @@ cd "$DSCANNER_DIR/tests" # IDE APIs # -------- # checking that reporting format stays consistent or only gets extended -diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json) +if [[ $1 == "Windows" ]]; then + diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix_windows.report.json) +else + diff <(../bin/dscanner --report it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.report.json) +fi + diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json) # CLI tests diff --git a/tests/it/autofix_ide/source_autofix.autofix.json b/tests/it/autofix_ide/source_autofix.autofix.json index fa4c066b..b66071d8 100644 --- a/tests/it/autofix_ide/source_autofix.autofix.json +++ b/tests/it/autofix_ide/source_autofix.autofix.json @@ -1,36 +1,36 @@ [ { - "name": "Mark function `const`", + "name": "Insert `const`", "replacements": [ { - "newText": " const", + "newText": "const ", "range": [ - 24, - 24 + 25, + 25 ] } ] }, { - "name": "Mark function `inout`", + "name": "Insert `inout`", "replacements": [ { - "newText": " inout", + "newText": "inout ", "range": [ - 24, - 24 + 25, + 25 ] } ] }, { - "name": "Mark function `immutable`", + "name": "Insert `immutable`", "replacements": [ { - "newText": " immutable", + "newText": "immutable ", "range": [ - 24, - 24 + 25, + 25 ] } ] diff --git a/tests/it/autofix_ide/source_autofix.report.json b/tests/it/autofix_ide/source_autofix.report.json index 909fa231..e639ce9a 100644 --- a/tests/it/autofix_ide/source_autofix.report.json +++ b/tests/it/autofix_ide/source_autofix.report.json @@ -18,37 +18,37 @@ "type": "warn", "autofixes": [ { - "name": "Mark function `const`", + "name": "Insert `const`", "replacements": [ { - "newText": " const", + "newText": "const ", "range": [ - 24, - 24 + 25, + 25 ] } ] }, { - "name": "Mark function `inout`", + "name": "Insert `inout`", "replacements": [ { - "newText": " inout", + "newText": "inout ", "range": [ - 24, - 24 + 25, + 25 ] } ] }, { - "name": "Mark function `immutable`", + "name": "Insert `immutable`", "replacements": [ { - "newText": " immutable", + "newText": "immutable ", "range": [ - 24, - 24 + 25, + 25 ] } ] @@ -71,10 +71,53 @@ }, { "name": "Wrap '{}' block around 'if'", - "replacements": "resolvable" + "replacements": [ + { + "newText": " {\n\t\t\t", + "range": [ + 69, + 69 + ] + }, + { + "newText": "\t", + "range": [ + 76, + 76 + ] + }, + { + "newText": "\t", + "range": [ + 80, + 80 + ] + }, + { + "newText": "\t", + "range": [ + 82, + 82 + ] + }, + { + "newText": "\t", + "range": [ + 84, + 84 + ] + }, + { + "newText": "}\n\t", + "range": [ + 85, + 85 + ] + } + ] } ], - "column": 3, + "column": 8, "endColumn": 10, "endIndex": 71, "endLine": 8, @@ -88,8 +131,8 @@ "type": "warn" } ], - "lineOfCodeCount": 3, - "statementCount": 4, + "lineOfCodeCount": 0, + "statementCount": 2, "structCount": 1, "templateCount": 0, "undocumentedPublicSymbols": 0 diff --git a/tests/it/autofix_ide/source_autofix_windows.report.json b/tests/it/autofix_ide/source_autofix_windows.report.json new file mode 100644 index 00000000..3132db7b --- /dev/null +++ b/tests/it/autofix_ide/source_autofix_windows.report.json @@ -0,0 +1,139 @@ +{ + "classCount": 0, + "functionCount": 1, + "interfaceCount": 0, + "issues": [ + { + "column": 6, + "endColumn": 12, + "endIndex": 22, + "endLine": 3, + "fileName": "it/autofix_ide/source_autofix.d", + "index": 16, + "key": "dscanner.confusing.function_attributes", + "line": 3, + "message": "Zero-parameter '@property' function should be marked 'const', 'inout', or 'immutable'.", + "name": "function_attribute_check", + "supplemental": [], + "type": "warn", + "autofixes": [ + { + "name": "Insert `const`", + "replacements": [ + { + "newText": "const ", + "range": [ + 25, + 25 + ] + } + ] + }, + { + "name": "Insert `inout`", + "replacements": [ + { + "newText": "inout ", + "range": [ + 25, + 25 + ] + } + ] + }, + { + "name": "Insert `immutable`", + "replacements": [ + { + "newText": "immutable ", + "range": [ + 25, + 25 + ] + } + ] + } + ] + }, + { + "autofixes": [ + { + "name": "Insert `static`", + "replacements": [ + { + "newText": "static ", + "range": [ + 69, + 69 + ] + } + ] + }, + { + "name": "Wrap '{}' block around 'if'", + "replacements": [ + { + "newText": " {\r\n\t\t\t", + "range": [ + 69, + 69 + ] + }, + { + "newText": "\t", + "range": [ + 76, + 76 + ] + }, + { + "newText": "\t", + "range": [ + 80, + 80 + ] + }, + { + "newText": "\t", + "range": [ + 82, + 82 + ] + }, + { + "newText": "\t", + "range": [ + 84, + 84 + ] + }, + { + "newText": "}\r\n\t", + "range": [ + 85, + 85 + ] + } + ] + } + ], + "column": 8, + "endColumn": 10, + "endIndex": 71, + "endLine": 8, + "fileName": "it/autofix_ide/source_autofix.d", + "index": 64, + "key": "dscanner.suspicious.static_if_else", + "line": 8, + "message": "Mismatched static if. Use 'else static if' here.", + "name": "static_if_else_check", + "supplemental": [], + "type": "warn" + } + ], + "lineOfCodeCount": 0, + "statementCount": 2, + "structCount": 1, + "templateCount": 0, + "undocumentedPublicSymbols": 0 +} \ No newline at end of file