Compare commits

...

48 Commits

Author SHA1 Message Date
WebFreak001 dc907e4a24 upgrade libdparse 2025-03-23 13:56:16 +01:00
Jan Jurzitza 3a87c65bac
Update actions/upload-artifact 2025-03-01 01:01:41 +00:00
Hiroki Noda 796d212b05 Fix: add build type for macos-13 runner with dmd 2024-05-06 11:31:18 +02:00
Hiroki Noda a8c4a588b2 CI: specify macos-13 for DMD 2024-05-06 11:31:18 +02:00
Hiroki Noda cc1a2c0178 CI: update actions/checkout to v4 2024-05-06 11:31:18 +02:00
Hiroki Noda ff0a9bc2ee CI: restrict dmd to macOS latest 2024-05-06 11:31:18 +02:00
Hiroki Noda 565087aa76 [ci skip]: use indent style for yaml 2024-05-06 10:42:51 +02:00
Hiroki Noda fe8f7bd8bc chore: remove travis related things 2024-05-06 10:11:11 +02:00
Hiroki Noda 22c9f980ae Allow skipping checks for dscanner.suspicious.unmodified with nolint 2024-05-06 10:10:54 +02:00
Hiroki Noda 17f3286fef Clearify key names 2024-05-06 10:08:53 +02:00
ryuukk 433d1eb73e Print to stdout 2024-02-08 03:46:26 +01:00
SixthDot 9076f7bab3
docs(dscanner/utils): Update obsolete url in comment (#944)
Co-authored-by: Petar Kirov <petar.p.kirov@gmail.com>
2024-01-01 11:08:09 +02:00
Jeremy Baxter 01e90ec4d8 Fix build on BSD
Removed the line `SHELL:=/usr/bin/env bash'. Most BSDs don't ship bash in the
base system by default and the build doesn't need it anyway.

Also added some more version statements to define useXDG for the other BSDs.
2023-12-26 13:10:01 +01:00
WebFreak001 8612841365 fix compilation on old compilers 2023-10-25 08:49:37 +02:00
WebFreak001 42033dcc55 add BaseAnalyzerArguments to keep ctor changes sane
also immediately makes tokens a part of it

This struct can for example precompute token indices for line endings
2023-10-25 08:49:37 +02:00
ricardaxel 1e8f1ec9e6
Allow skipping checks with @("nolint(...)") and @nolint("...") (#936)
Co-authored-by: Axel Ricard <contact@axelricard.fr>
Co-authored-by: WebFreak001 <gh@webfreak.org>
2023-10-13 02:45:59 +02:00
Axel Ricard 69d824f4f7 introduce variable expandedArgs 2023-10-11 00:34:00 +02:00
Axel Ricard 3bf3f25f9a add --exclude cli option
This excludes given files or directory from linting
2023-10-11 00:34:00 +02:00
Axel Ricard 87f85c7db7 add some utils functions for path manipulation 2023-10-11 00:34:00 +02:00
Prajwal S N 159e9c9eec feat(highlight): support multiple themes
Signed-off-by: Prajwal S N <prajwalnadig21@gmail.com>
2023-09-24 19:36:21 +02:00
Robert Schadek b43c8f45cf Always Check Curly
Check that if|else|for|foreach|while|do|try|catch
are always followed by a BlockStatement aka. { }

closer

can not get the test to work

try to get the AutoFix in place

maybe a fix

nicer messages

some formatting

more tinkering

still nothing

autofix work now

AutoFix name

message to message_postfix
2023-09-24 19:35:46 +02:00
WebFreak001 fc1699bb97 simplify it.sh 2023-09-24 15:31:50 +02:00
WebFreak001 6491d792f5 support `@arguments.rst` for args through file 2023-09-24 15:31:50 +02:00
WebFreak001 a958f9ac7b fix unused variable check for unitthreaded checks
such as `a.should == b`
2023-07-17 14:41:07 +02:00
WebFreak001 c8262f4220 fix auto_function autofix for `auto ref fn()` 2023-07-17 11:32:16 +02:00
WebFreak001 f22b2e587c Disable auto_function_check by default
Since it may be used to auto-infer function attributes
2023-07-17 11:32:16 +02:00
WebFreak001 5d67707744 more sane parentheses fix for delegates
not sure what I was thinking with the initial version
2023-07-13 16:42:36 +02:00
WebFreak001 7601fe65f9 fix diagnostic location for `@UDA auto f() {}` 2023-07-10 22:05:26 +02:00
WebFreak001 c1e051bfba fix infinite allocating in context formatter 2023-07-10 13:57:27 +02:00
WebFreak001 48db254fb0 fix if scopes and shortened function bodies 2023-07-10 00:52:04 +02:00
WebFreak001 d275361153 fix case/default scopes, fix #913 2023-07-10 00:52:04 +02:00
Jan Jurzitza fed654441f
fix #916 autofix CLI, add integration test for it (#917) 2023-07-09 13:09:21 +02:00
Jan Jurzitza 4c759b072c
include resolved autofixes in `--report` output (#915) 2023-07-09 09:44:02 +02:00
WebFreak001 cae7d595b8 checkout IT files with LF line endings 2023-07-09 00:45:42 +02:00
WebFreak001 5d3296cc0b it: only rebuild dscanner outside CI
fix windows redirects
2023-07-09 00:45:42 +02:00
WebFreak001 d7e15903dd run integration tests in CI 2023-07-09 00:45:42 +02:00
WebFreak001 0590376045 add IDE integration tests 2023-07-08 23:09:33 +02:00
WebFreak001 3345a1953a improve public auto fix API 2023-07-08 23:09:33 +02:00
WebFreak001 43caad72a8 add CLI API to get autofixes at location 2023-07-08 23:09:33 +02:00
WebFreak001 53c9536332 add startIndex, endIndex support to format string 2023-07-08 23:09:33 +02:00
WebFreak001 4194e6af0c add `dscanner fix` command 2023-07-08 23:09:33 +02:00
WebFreak001 48cec8a6f4 implement indentation aware autofixes 2023-07-08 23:09:33 +02:00
WebFreak001 93aae57469 add autofix testing API 2023-07-08 23:09:33 +02:00
WebFreak001 f12319d5a8 add autofix whitespace collapsing API 2023-07-08 23:09:33 +02:00
WebFreak001 513b7dafc3 add auto-fix API 2023-07-08 23:09:33 +02:00
Christian Köstlin 35d2cf4177 feature: Provide predefined error format compatible with dmds output
an output parser that works with dmd / ldc just works (tm) witha
dscanner as well
2023-07-07 00:15:04 +02:00
WebFreak001 1201a68f66 Only call Win32 API to enable colored output once 2023-07-05 22:41:42 +02:00
WebFreak001 b0bb905a40 upgrade libdparse to 0.23.2 fix mixin-type VarDecl 2023-07-05 21:37:52 +02:00
84 changed files with 3558 additions and 700 deletions

View File

@ -19,3 +19,7 @@ dfmt_space_after_keywords = true
dfmt_selective_import_space = true
dfmt_compact_labeled_statements = true
dfmt_template_constraint_style = conditional_newline_indent
[*.yml]
indent_style = space
indent_size = 2

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
tests/it/autofix_ide/source_autofix.d text eol=lf

View File

@ -57,6 +57,11 @@ jobs:
dmd: gdc-12
host: macos-latest
# Restrict DMD to macOS latest
- compiler:
dmd: dmd
host: macos-latest
# Omit dub builds for GDC because dub rejects the old fronted revision
- compiler:
dmd: gdc-12
@ -65,12 +70,19 @@ jobs:
include:
- { do_report: 1, build: { type: dub, version: 'current' }, host: 'ubuntu-22.04', compiler: { version: dmd-latest, dmd: dmd } }
- compiler:
dmd: dmd
host: macos-13
build:
type: 'dub'
version: 'current'
runs-on: ${{ matrix.host }}
steps:
# Clone repo + submodules
- name: Checkout repo
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: 'recursive'
fetch-depth: 0
@ -123,7 +135,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
@ -146,9 +158,14 @@ jobs:
fi
"./bin/dscanner$EXE" --styleCheck -f "$FORMAT" src
- name: Integration Tests
run: ./it.sh
working-directory: tests
shell: bash
# Parse phobos to check for failures / crashes / ...
- name: Checkout Phobos
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
repository: dlang/phobos
path: phobos

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -e
if [[ $BUILD == dub ]]; then
if [[ -n $LIBDPARSE_VERSION ]]; then
rdmd ./d-test-utils/test_with_package.d $LIBDPARSE_VERSION libdparse -- dub test
elif [[ -n $DSYMBOL_VERSION ]]; then
rdmd ./d-test-utils/test_with_package.d $DSYMBOL_VERSION dsymbol -- dub test
else
echo 'Cannot run test without LIBDPARSE_VERSION nor DSYMBOL_VERSION environment variable'
exit 1
fi
elif [[ $DC == ldc2 ]]; then
git submodule update --init --recursive
make test DC=ldmd2
else
git submodule update --init --recursive
make test
make lint
git clone https://www.github.com/dlang/phobos.git --depth=1
# just check that it doesn't crash
cd phobos/std && ../../bin/dscanner -S || true
fi

View File

@ -1,110 +0,0 @@
dist: xenial
sudo: false
language: d
d:
- dmd
- ldc-beta
- ldc
os:
- linux
- osx
env:
- BUILD=
- BUILD=dub LIBDPARSE_VERSION=min
- BUILD=dub LIBDPARSE_VERSION=max
- BUILD=dub DSYMBOL_VERSION=min
- BUILD=dub DSYMBOL_VERSION=max
branches:
only:
- master
- /^v\d+\.\d+\.\d+([+-]\S*)*$/
script: "./.travis.sh"
jobs:
include:
- stage: GitHub Release
#if: tag IS present
d: ldc-1.13.0
os: linux
script: echo "Deploying to GitHub releases ..." && ./release.sh
deploy:
provider: releases
api_key: $GH_REPO_TOKEN
file_glob: true
file: bin/dscanner-*.tar.gz
skip_cleanup: true
on:
repo: dlang-community/D-Scanner
tags: true
- stage: GitHub Release
#if: tag IS present
d: ldc-1.13.0
os: osx
script: echo "Deploying to GitHub releases ..." && ./release.sh
deploy:
provider: releases
api_key: $GH_REPO_TOKEN
file_glob: true
file: bin/dscanner-*.tar.gz
skip_cleanup: true
on:
repo: dlang-community/D-Scanner
tags: true
- stage: GitHub Release
#if: tag IS present
d: dmd
os: linux
language: generic
script: echo "Deploying to GitHub releases ..." && ./release-windows.sh
addons:
apt:
packages:
- p7zip-full
deploy:
provider: releases
api_key: $GH_REPO_TOKEN
file_glob: true
file: bin/dscanner-*.zip
skip_cleanup: true
on:
repo: dlang-community/D-Scanner
tags: true
- stage: GitHub Release
#if: tag IS present
d: dmd
os: linux
language: generic
script: echo "Deploying to GitHub releases ..." && ARCH=64 ./release-windows.sh
addons:
apt:
packages:
- p7zip-full
deploy:
provider: releases
api_key: $GH_REPO_TOKEN
file_glob: true
file: bin/dscanner-*.zip
skip_cleanup: true
on:
repo: dlang-community/D-Scanner
tags: true
- stage: dockerhub-stable
if: tag IS present
d: ldc
os: linux
script:
- echo "Deploying to DockerHub..." && ./release.sh
- LATEST_TAG="$(git describe --abbrev=0 --tags)"
- docker build -t "dlangcommunity/dscanner:${LATEST_TAG} ."
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]] ; then docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" ; fi
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]] ; then docker push "dlangcommunity/dscanner:${LATEST_TAG}" ; fi
- stage: dockerhub-latest
d: ldc
os: linux
script:
- echo "Deploying to DockerHub..." && ./release.sh
- docker build -t dlangcommunity/dscanner:latest .
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]] ; then docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" ; fi
- if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]] ; then docker push dlangcommunity/dscanner:latest ; fi
stages:
- name: test
if: type = pull_request or (type = push and branch = master)

View File

@ -57,6 +57,32 @@ dscanner lint source/
to view a human readable list of issues.
Diagnostic types can be enabled/disabled using a configuration file, check out
the `--config` argument / `dscanner.ini` file for more info. Tip: some IDEs that
integrate D-Scanner may have helpers to configure the diagnostics or help
generate the dscanner.ini file.
<!--
IDE list for overview:
code-d has an "insert default dscanner.ini content" command + proprietary
disabling per-line (we really need to bring that into standard D-Scanner)
-->
## Auto-Fixing issues
Use
```sh
dscanner fix source/
```
to interactively fix all fixable issues within the source directory. Call with
`--applySingle` to automatically apply fixes that don't have multiple automatic
solutions.
## Tooling integration
Many D editors already ship with D-Scanner.
For a CLI / tool parsable output use either
```sh
@ -65,6 +91,11 @@ dscanner -S source/
dscanner --report source/
```
The `--report` switch includes all information, plus cheap to compute autofixes
that are already resolved ahead of time, as well as the names for the autofixes
that need to be resolved using the `--resolveMessage` switch like described
below.
You can also specify custom formats using `-f` / `--errorFormat`, where there
are also built-in formats for GitHub Actions:
@ -75,15 +106,47 @@ dscanner -S -f github source/
dscanner -S -f '{filepath}({line}:{column})[{type}]: {message}' source/
```
Diagnostic types can be enabled/disabled using a configuration file, check out
the `--config` argument / `dscanner.ini` file for more info. Tip: some IDEs that
integrate D-Scanner may have helpers to configure the diagnostics or help
generate the dscanner.ini file.
<!--
IDE list for overview:
code-d has an "insert default dscanner.ini content" command + proprietary
disabling per-line (we really need to bring that into standard D-Scanner)
-->
To resolve automatic issue fixes for a given location use
```sh
# collecting automatic issue fixes
# --resolveMessage <line>:<column> <filename>
dscanner --resolveMessage 11:3 file.d
# --resolveMessage b<byteIndex> <filename>
dscanner --resolveMessage b512 file.d
# <filename> may be omitted to read from stdin
```
outputs JSON:
```json
// list of available auto-fixes at the given location
[
{
"name": "Make function const",
// byte range `[start, end)` what code to replace
// this is sorted by range[0]
"replacements": [
// replace: range[0] < range[1], newText != ""
{"range": [10, 14], "newText": "const "},
// insert: range[0] == range[1], newText != ""
{"range": [20, 20], "newText": "auto"},
// remove: range[0] < range[1], newText == ""
{"range": [30, 40], "newText": ""},
]
}
]
```
Algorithm to apply replacements:
```d
foreach_reverse (r; replacements)
codeBytes = codeBytes[0 .. r.range[0]] ~ r.newText ~ codeBytes[r.range[1] .. $];
```
Replacements are non-overlapping, sorted by `range[0]` in ascending order. When
combining multiple different replacements, you first need to sort them by
`range[0]` to apply using the algorithm above.
## Other features
@ -139,7 +202,7 @@ To avoid these cases, it's possible to pass the "--skipTests" option.
#### Configuration
By default all checks are enabled. Individual checks can be enabled or disabled
by using a configuration file. Such a file can be placed, for example, is the root directory of your project.
Running ```dscanner --defaultConfig``` will generate a default configuration file and print the file's location.
Running `dscanner --defaultConfig` will generate a default configuration file and print the file's location.
You can also specify the path to a configuration file by using the "--config" option if
you want to override the default or the local settings.
@ -243,8 +306,15 @@ and case tokens in the file.
### Syntax Highlighting
The "--highlight" option prints the given source file as syntax-highlighted HTML
to the standard output. The CSS styling is currently hard-coded to use the
[Solarized](http://ethanschoonover.com/solarized) color scheme.
to the standard output. The CSS styling uses the [Solarized](http://ethanschoonover.com/solarized)
color scheme by default, but can be customised using the "--theme" option.
The following themes are available:
- `solarized`
- `solarized-dark`
- `gruvbox`
- `gruvbox-dark`
No example. It would take up too much space

View File

@ -11,7 +11,7 @@
"built_with_dub"
],
"dependencies": {
"libdparse": ">=0.23.1 <0.24.0",
"libdparse": ">=0.23.1 <0.26.0",
"dcd:dsymbol": ">=0.16.0-beta.2 <0.17.0",
"inifiled": "~>1.3.1",
"emsi_containers": "~>0.9.0",

View File

@ -6,7 +6,7 @@
"emsi_containers": "0.9.0",
"inifiled": "1.3.3",
"libddoc": "0.8.0",
"libdparse": "0.23.1",
"libdparse": "0.25.0",
"stdx-allocator": "2.77.5"
}
}

@ -1 +1 @@
Subproject commit e354f917f20c4a1fae04d1680205486c2a2a8317
Subproject commit f8a6c28589aae180532fb460a1b22e92a0978292

View File

@ -86,8 +86,6 @@ else ifneq (,$(findstring gdc, $(DC)))
WRITE_TO_TARGET_NAME = -o $@
endif
SHELL:=/usr/bin/env bash
GITHASH = bin/githash.txt

View File

@ -18,9 +18,9 @@ final class AliasSyntaxCheck : BaseAnalyzer
mixin AnalyzerInfo!"alias_syntax_check";
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const AliasDeclaration ad)

View File

@ -30,9 +30,9 @@ final class AllManCheck : BaseAnalyzer
mixin AnalyzerInfo!"allman_braces_check";
///
this(string fileName, const(Token)[] tokens, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
foreach (i; 1 .. tokens.length - 1)
{
const curLine = tokens[i].line;

View File

@ -0,0 +1,227 @@
// 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.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
{
mixin AnalyzerInfo!"always_curly_check";
alias visit = BaseAnalyzer.visit;
///
this(BaseAnalyzerArguments args)
{
super(args);
}
void test(L, B)(L loc, B s, string stmtKind)
{
if (!is(s == BlockStatement))
{
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]);
}
else
{
addErrorMessage(loc, KEY, stmtKind ~ MESSAGE_POSTFIX);
}
}
}
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");
}
}
override void visit(const(ForStatement) stmt)
{
auto s = stmt.declarationOrStatement;
if (s.statement !is null)
{
this.test(s, s, "for");
}
}
override void visit(const(ForeachStatement) stmt)
{
auto s = stmt.declarationOrStatement;
if (s.statement !is null)
{
this.test(s, s, "foreach");
}
}
override void visit(const(TryStatement) stmt)
{
auto s = stmt.declarationOrStatement;
if (s.statement !is null)
{
this.test(s, s, "try");
}
if (stmt.catches !is null)
{
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");
}
}
}
}
override void visit(const(WhileStatement) stmt)
{
auto s = stmt.declarationOrStatement;
if (s.statement !is null)
{
this.test(s, s, "while");
}
}
override void visit(const(DoStatement) stmt)
{
auto s = stmt.statementNoCaseNoDefault;
if (s !is null)
{
this.test(s, s, "do");
}
}
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 std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.always_curly_check = Check.enabled;
assertAnalyzerWarnings(q{
void testIf()
{
if(true) return; // [warn]: if must be follow by a BlockStatement aka. { }
}
}, sac);
assertAnalyzerWarnings(q{
void testIf()
{
if(true) return; /+
^^^^^^^ [warn]: if must be follow by a BlockStatement aka. { } +/
}
}, sac);
assertAnalyzerWarnings(q{
void testIf()
{
for(int i = 0; i < 10; ++i) return; // [warn]: for must be follow by a BlockStatement aka. { }
}
}, sac);
assertAnalyzerWarnings(q{
void testIf()
{
foreach(it; 0 .. 10) return; // [warn]: foreach must be follow by a BlockStatement aka. { }
}
}, sac);
assertAnalyzerWarnings(q{
void testIf()
{
while(true) return; // [warn]: while must be follow by a BlockStatement aka. { }
}
}, sac);
assertAnalyzerWarnings(q{
void testIf()
{
do return; while(true); return; // [warn]: do must be follow by a BlockStatement aka. { }
}
}, sac);
}
unittest {
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.always_curly_check = Check.enabled;
assertAutoFix(q{
void test() {
if(true) return; // fix:0
}
}c, q{
void test() {
if(true) { return; } // fix:0
}
}c, sac);
assertAutoFix(q{
void test() {
foreach(_; 0 .. 10 ) return; // fix:0
}
}c, q{
void test() {
foreach(_; 0 .. 10 ) { return; } // fix:0
}
}c, sac);
assertAutoFix(q{
void test() {
for(int i = 0; i < 10; ++i) return; // fix:0
}
}c, q{
void test() {
for(int i = 0; i < 10; ++i) { return; } // fix:0
}
}c, sac);
assertAutoFix(q{
void test() {
do return; while(true) // fix:0
}
}c, q{
void test() {
do { return; } while(true) // fix:0
}
}c, sac);
stderr.writeln("Unittest for AlwaysCurly passed.");
}

View File

@ -22,9 +22,9 @@ final class AsmStyleCheck : BaseAnalyzer
mixin AnalyzerInfo!"asm_style_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const AsmBrExp brExp)
@ -32,11 +32,13 @@ final class AsmStyleCheck : BaseAnalyzer
if (brExp.asmBrExp !is null && brExp.asmBrExp.asmUnaExp !is null
&& brExp.asmBrExp.asmUnaExp.asmPrimaryExp !is null)
{
addErrorMessage(brExp, "dscanner.confusing.brexp",
addErrorMessage(brExp, KEY,
"This is confusing because it looks like an array index. Rewrite a[1] as [a + 1] to clarify.");
}
brExp.accept(this);
}
private enum string KEY = "dscanner.confusing.brexp";
}
unittest

View File

@ -23,9 +23,9 @@ final class AssertWithoutMessageCheck : BaseAnalyzer
mixin AnalyzerInfo!"assert_without_msg";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const AssertExpression expr)

View File

@ -40,21 +40,22 @@ public:
mixin AnalyzerInfo!"auto_function_check";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
package static const(Token)[] findAutoReturnType(const(FunctionDeclaration) decl)
{
auto autoFunTokens = decl.storageClasses
.map!(a => a.token.type == tok!"auto"
? [a.token]
: a.atAttribute
? a.atAttribute.tokens
: null)
.filter!(a => a.length > 0);
return autoFunTokens.empty ? null : autoFunTokens.front;
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;
}
override void visit(const(FunctionDeclaration) decl)
@ -76,10 +77,13 @@ public:
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);
addErrorMessage([whitespaceIndex, whitespaceIndex + 1], tok.line, [whitespace, whitespace + 1], KEY, MESSAGE_INSERT,
[AutoFix.insertionAt(whitespaceIndex + 1, "void ")]);
}
else
addErrorMessage(autoTokens, KEY, MESSAGE);
addErrorMessage(autoTokens, KEY, MESSAGE,
[AutoFix.replacement(autoTokens[0], "", "Replace `auto` with `void`")
.concat(AutoFix.insertionAt(decl.name.index, "void "))]);
}
}
@ -193,6 +197,9 @@ unittest
^^^^ [warn]: %s +/
auto doStuff(){} /+
^^^^ [warn]: %s +/
@Custom
auto doStuff(){} /+
^^^^ [warn]: %s +/
int doStuff(){auto doStuff(){}} /+
^^^^ [warn]: %s +/
auto doStuff(){return 0;}
@ -201,6 +208,7 @@ unittest
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
), sac);
assertAnalyzerWarnings(q{
@ -268,5 +276,22 @@ unittest
auto doStuff(){ mixin(_genSave);}
}, sac);
assertAutoFix(q{
auto ref doStuff(){} // fix
auto doStuff(){} // fix
@property doStuff(){} // fix
@safe doStuff(){} // fix
@Custom
auto doStuff(){} // fix
}c, q{
ref void doStuff(){} // fix
void doStuff(){} // fix
@property void doStuff(){} // fix
@safe void doStuff(){} // fix
@Custom
void doStuff(){} // fix
}c, sac);
stderr.writeln("Unittest for AutoFunctionChecker passed.");
}

View File

@ -17,9 +17,9 @@ final class AutoRefAssignmentCheck : BaseAnalyzer
mixin AnalyzerInfo!"auto_ref_assignment_check";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const Module m)

View File

@ -1,14 +1,265 @@
module dscanner.analysis.base;
import std.container;
import std.string;
import dparse.ast;
import std.array;
import dparse.lexer : IdType, str, Token, tok;
import dscanner.analysis.nolint;
import dsymbol.scope_ : Scope;
import dparse.lexer : Token, str, IdType;
import std.array;
import std.container;
import std.meta : AliasSeq;
import std.string;
import std.sumtype;
///
struct AutoFix
{
///
struct CodeReplacement
{
/// Byte index `[start, end)` within the file what text to replace.
/// `start == end` if text is only getting inserted.
size_t[2] range;
/// The new text to put inside the range. (empty to delete text)
string newText;
}
/// Context that the analyzer resolve method can use to generate the
/// resolved `CodeReplacement` with.
struct ResolveContext
{
/// Arbitrary analyzer-defined parameters. May grow in the future with
/// more items.
ulong[3] params;
/// For dynamically sized data, may contain binary data.
string extraInfo;
}
/// Display name for the UI.
string name;
/// Either code replacements, sorted by range start, never overlapping, or a
/// context that can be passed to `BaseAnalyzer.resolveAutoFix` along with
/// the message key from the parent `Message` object.
///
/// `CodeReplacement[]` should be applied to the code in reverse, otherwise
/// an offset to the following start indices must be calculated and be kept
/// track of.
SumType!(CodeReplacement[], ResolveContext) replacements;
invariant
{
replacements.match!(
(const CodeReplacement[] replacement)
{
import std.algorithm : all, isSorted;
assert(replacement.all!"a.range[0] <= a.range[1]");
assert(replacement.isSorted!"a.range[0] < b.range[0]");
},
(_) {}
);
}
static AutoFix resolveLater(string name, ulong[3] params, string extraInfo = null)
{
AutoFix ret;
ret.name = name;
ret.replacements = ResolveContext(params, extraInfo);
return ret;
}
static AutoFix replacement(const Token token, string newText, string name = null)
{
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);
}
static AutoFix replacement(size_t[2] range, string newText, string name)
{
AutoFix ret;
ret.name = name;
ret.replacements = [
AutoFix.CodeReplacement(range, newText)
];
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");
AutoFix ret;
ret.name = name.length
? name
: content.strip.length
? "Insert `" ~ content.strip ~ "`"
: "Insert whitespace";
ret.replacements = [
AutoFix.CodeReplacement([index, index], content)
];
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;
static immutable string errorMsg = "Cannot concatenate code replacement with late-resolve";
AutoFix ret;
ret.name = name;
CodeReplacement[] concatenated = expectReplacements(errorMsg).dup
~ other.expectReplacements(errorMsg);
concatenated.sort!"a.range[0] < b.range[0]";
ret.replacements = concatenated;
return ret;
}
CodeReplacement[] expectReplacements(
string errorMsg = "Expected available code replacements, not something to resolve later"
) @safe pure nothrow @nogc
{
return replacements.match!(
(replacement)
{
if (false) return CodeReplacement[].init;
static if (is(immutable typeof(replacement) == immutable CodeReplacement[]))
return replacement;
else
assert(false, errorMsg);
}
);
}
const(CodeReplacement[]) expectReplacements(
string errorMsg = "Expected available code replacements, not something to resolve later"
) const @safe pure nothrow @nogc
{
return replacements.match!(
(const replacement)
{
if (false) return CodeReplacement[].init;
static if (is(immutable typeof(replacement) == immutable CodeReplacement[]))
return replacement;
else
assert(false, errorMsg);
}
);
}
}
/// Formatting style for autofix generation (only available for resolve autofix)
struct AutoFixFormatting
{
enum AutoFixFormatting invalid = AutoFixFormatting(BraceStyle.invalid, null, 0, null);
enum BraceStyle
{
/// invalid, shouldn't appear in usable configs
invalid,
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Allman_style)
allman,
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Variant:_1TBS)
otbs,
/// $(LINK https://en.wikipedia.org/wiki/Indent_style#Variant:_Stroustrup)
stroustrup,
/// $(LINK https://en.wikipedia.org/wiki/Indentation_style#K&R_style)
knr,
}
/// Brace style config
BraceStyle braceStyle = BraceStyle.allman;
/// String to insert on indentations
string indentation = "\t";
/// For calculating indentation size
uint indentationWidth = 4;
/// String to insert on line endings
string eol = "\n";
invariant
{
import std.algorithm : all;
assert(!indentation.length
|| indentation == "\t"
|| indentation.all!(c => c == ' '));
}
string getWhitespaceBeforeOpeningBrace(string lastLineIndent, bool isFuncDecl) pure nothrow @safe const
{
final switch (braceStyle)
{
case BraceStyle.invalid:
assert(false, "invalid formatter config");
case BraceStyle.knr:
if (isFuncDecl)
goto case BraceStyle.allman;
else
goto case BraceStyle.otbs;
case BraceStyle.otbs:
case BraceStyle.stroustrup:
return " ";
case BraceStyle.allman:
return eol ~ lastLineIndent;
}
}
}
/// A diagnostic message. Each message defines one issue in the file, which
/// consists of one or more squiggly line ranges within the code, as well as
/// human readable descriptions and optionally also one or more automatic code
/// fixes that can be applied.
struct Message
{
/// A squiggly line range within the code. May be the issue itself if it's
/// the `diagnostic` member or supplemental information that can aid the
/// user in resolving the issue.
struct Diagnostic
{
/// Name of the file where the warning was triggered.
@ -22,8 +273,6 @@ struct Message
/// Warning message, may be null for supplemental diagnostics.
string message;
// TODO: add auto-fix suggestion API here
deprecated("Use startLine instead") alias line = startLine;
deprecated("Use startColumn instead") alias column = startColumn;
@ -74,6 +323,10 @@ struct Message
/// Check name
string checkName;
/// Either immediate code changes that can be applied or context to call
/// 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)
{
diagnostic.fileName = fileName;
@ -84,19 +337,21 @@ struct Message
this.checkName = checkName;
}
this(Diagnostic diagnostic, string key = null, string checkName = null)
this(Diagnostic diagnostic, string key = null, string checkName = null, AutoFix[] autofixes = null)
{
this.diagnostic = diagnostic;
this.key = key;
this.checkName = checkName;
this.autofixes = autofixes;
}
this(Diagnostic diagnostic, Diagnostic[] supplemental, string key = null, string checkName = null)
this(Diagnostic diagnostic, Diagnostic[] supplemental, string key = null, string checkName = null, AutoFix[] autofixes = null)
{
this.diagnostic = diagnostic;
this.supplemental = supplemental;
this.key = key;
this.checkName = checkName;
this.autofixes = autofixes;
}
alias diagnostic this;
@ -116,18 +371,45 @@ mixin template AnalyzerInfo(string checkName)
}
}
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
{
public:
deprecated("Don't use this constructor, use the one taking BaseAnalyzerArguments")
this(string fileName, const Scope* sc, bool skipTests = false)
{
this.sc = sc;
this.fileName = fileName;
this.skipTests = skipTests;
BaseAnalyzerArguments args = {
fileName: fileName,
sc: sc,
skipTests: skipTests
};
this(args);
}
this(BaseAnalyzerArguments args)
{
this.sc = args.sc;
this.tokens = args.tokens;
this.fileName = args.fileName;
this.skipTests = args.skipTests;
_messages = new MessageSet;
}
protected string getName()
string getName()
{
assert(0);
}
@ -151,10 +433,55 @@ public:
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);
}
}
/**
* 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)
{
@ -169,43 +496,59 @@ protected:
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)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(fileName, line, column, key, message, getName()));
}
void addErrorMessage(const BaseNode node, string key, string message)
void addErrorMessage(const BaseNode node, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, node, message), key);
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, node, message), key, autofixes);
}
void addErrorMessage(const Token token, string key, string message)
void addErrorMessage(const Token token, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, token, message), key);
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, token, message), key, autofixes);
}
void addErrorMessage(const Token[] tokens, string key, string message)
void addErrorMessage(const Token[] tokens, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(Message.Diagnostic.from(fileName, tokens, message), key);
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)
void addErrorMessage(size_t[2] index, size_t line, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
addErrorMessage(index, [line, line], columns, key, message);
if (noLint.containsCheck(key))
return;
addErrorMessage(index, [line, line], columns, key, message, autofixes);
}
void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message)
void addErrorMessage(size_t[2] index, size_t[2] lines, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
auto d = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(d, key, getName()));
_messages.insert(Message(d, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, string key)
void addErrorMessage(Message.Diagnostic diagnostic, string key, AutoFix[] autofixes = null)
{
_messages.insert(Message(diagnostic, key, getName()));
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key)
void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key, AutoFix[] autofixes = null)
{
_messages.insert(Message(diagnostic, supplemental, key, getName()));
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, supplemental, key, getName(), autofixes));
}
/**
@ -231,3 +574,326 @@ const(Token)[] findTokenForDisplay(const Token[] tokens, IdType type, const(Toke
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;
}
SwitchStack[] switchStack;
}
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
{
this(size_t codeLine)
{
super(BaseAnalyzerArguments("stdin"));
this.codeLine = codeLine;
}
override void visit(const FunctionCallExpression f)
{
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")
{
assert(stack[$ - 1]);
}
}
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);
}
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;
});
}

View File

@ -12,9 +12,9 @@ final class BodyOnDisabledFuncsCheck : BaseAnalyzer
mixin AnalyzerInfo!"body_on_disabled_func_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
static foreach (AggregateType; AliasSeq!(InterfaceDeclaration, ClassDeclaration,

View File

@ -33,9 +33,9 @@ final class BuiltinPropertyNameCheck : BaseAnalyzer
mixin AnalyzerInfo!"builtin_property_names_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const FunctionDeclaration fd)

View File

@ -19,9 +19,9 @@ final class CommaExpressionCheck : BaseAnalyzer
mixin AnalyzerInfo!"comma_expression_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Expression ex)

View File

@ -165,7 +165,10 @@ struct StaticAnalysisConfig
string lambda_return_check = Check.enabled;
@INI("Check for auto function without return statement")
string auto_function_check = Check.enabled;
string auto_function_check = Check.disabled;
@INI("Check that if|else|for|foreach|while|do|try|catch are always followed by a BlockStatement { }")
string always_curly_check = Check.disabled;
@INI("Check for sortedness of imports")
string imports_sortedness = Check.disabled;
@ -220,6 +223,69 @@ struct StaticAnalysisConfig
@INI("Module-specific filters")
ModuleFilters filters;
@INI("Formatting brace style for automatic fixes (allman, otbs, stroustrup, knr)")
string brace_style = "allman";
@INI("Formatting indentation style for automatic fixes (tab, space)")
string indentation_style = "tab";
@INI("Formatting indentation width for automatic fixes (space count, otherwise how wide a tab is)")
int indentation_width = 4;
@INI("Formatting line ending character (lf, cr, crlf)")
string eol_style = "lf";
auto getAutoFixFormattingConfig() const
{
import dscanner.analysis.base : AutoFixFormatting;
import std.array : array;
import std.conv : to;
import std.range : repeat;
if (indentation_width < 0)
throw new Exception("invalid negative indentation_width");
AutoFixFormatting ret;
ret.braceStyle = brace_style.to!(AutoFixFormatting.BraceStyle);
ret.indentationWidth = indentation_width;
switch (indentation_style)
{
case "tab":
ret.indentation = "\t";
break;
case "space":
static immutable string someSpaces = " ";
if (indentation_width < someSpaces.length)
ret.indentation = someSpaces[0 .. indentation_width];
else
ret.indentation = ' '.repeat(indentation_width).array;
break;
default:
throw new Exception("invalid indentation_style: '" ~ indentation_style ~ "' (expected tab or space)");
}
switch (eol_style)
{
case "lf":
case "LF":
ret.eol = "\n";
break;
case "cr":
case "CR":
ret.eol = "\r";
break;
case "crlf":
case "CRLF":
ret.eol = "\r\n";
break;
default:
throw new Exception("invalid eol_style: '" ~ eol_style ~ "' (expected lf, cr or crlf)");
}
return ret;
}
}
private template ModuleFiltersMixin(A)

View File

@ -14,9 +14,9 @@ final class ConstructorCheck : BaseAnalyzer
mixin AnalyzerInfo!"constructor_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const ClassDeclaration classDeclaration)

View File

@ -53,10 +53,9 @@ final class CyclomaticComplexityCheck : BaseAnalyzer
int maxCyclomaticComplexity;
///
this(string fileName, const(Scope)* sc, bool skipTests = false,
int maxCyclomaticComplexity = 50)
this(BaseAnalyzerArguments args, int maxCyclomaticComplexity = 50)
{
super(fileName, sc, skipTests);
super(args);
this.maxCyclomaticComplexity = maxCyclomaticComplexity;
}

View File

@ -20,23 +20,27 @@ final class DeleteCheck : BaseAnalyzer
mixin AnalyzerInfo!"delete_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const DeleteExpression d)
{
addErrorMessage(d.tokens[0], "dscanner.deprecated.delete_keyword",
"Avoid using the 'delete' keyword.");
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);
}
private enum string KEY = "dscanner.deprecated.delete_keyword";
}
unittest
{
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings;
import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
StaticAnalysisConfig sac = disabledConfig();
sac.delete_check = Check.enabled;
@ -53,5 +57,25 @@ unittest
}
}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.");
}

View File

@ -23,9 +23,9 @@ final class DuplicateAttributeCheck : BaseAnalyzer
mixin AnalyzerInfo!"duplicate_attribute";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Declaration node)
@ -93,7 +93,8 @@ final class DuplicateAttributeCheck : BaseAnalyzer
if (hasAttribute)
{
string message = "Attribute '%s' is duplicated.".format(attributeName);
addErrorMessage(tokens, "dscanner.unnecessary.duplicate_attribute", message);
addErrorMessage(tokens, KEY, message,
[AutoFix.replacement(tokens, "", "Remove second attribute " ~ attributeName)]);
}
// Mark it as having that attribute
@ -148,6 +149,8 @@ final class DuplicateAttributeCheck : BaseAnalyzer
return null;
}
private enum string KEY = "dscanner.unnecessary.duplicate_attribute";
}
unittest
@ -226,5 +229,40 @@ unittest
}
}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.");
}

View File

@ -8,7 +8,7 @@ module dscanner.analysis.enumarrayliteral;
import dparse.ast;
import dparse.lexer;
import dscanner.analysis.base;
import std.algorithm : canFind, map;
import std.algorithm : find, map;
import dsymbol.scope_ : Scope;
void doNothing(string, size_t, size_t, string, bool)
@ -21,9 +21,9 @@ final class EnumArrayLiteralCheck : BaseAnalyzer
mixin AnalyzerInfo!"enum_array_literal_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
bool looking;
@ -35,7 +35,8 @@ final class EnumArrayLiteralCheck : BaseAnalyzer
override void visit(const AutoDeclaration autoDec)
{
if (autoDec.storageClasses.canFind!(a => a.token == tok!"enum"))
auto enumToken = autoDec.storageClasses.find!(a => a.token == tok!"enum");
if (enumToken.length)
{
foreach (part; autoDec.parts)
{
@ -46,12 +47,39 @@ final class EnumArrayLiteralCheck : BaseAnalyzer
if (part.initializer.nonVoidInitializer.arrayInitializer is null)
continue;
addErrorMessage(part.initializer.nonVoidInitializer,
"dscanner.performance.enum_array_literal",
KEY,
"This enum may lead to unnecessary allocation at run-time."
~ " Use 'static immutable "
~ part.identifier.text ~ " = [ ...' instead.");
~ part.identifier.text ~ " = [ ...' instead.",
[
AutoFix.replacement(enumToken[0].token, "static immutable")
]);
}
}
autoDec.accept(this);
}
private enum string KEY = "dscanner.performance.enum_array_literal";
}
unittest
{
import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, 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. +/
}c, sac);
assertAutoFix(q{
enum x = [1, 2, 3]; // fix
}c, q{
static immutable x = [1, 2, 3]; // fix
}c, sac);
stderr.writeln("Unittest for EnumArrayLiteralCheck passed.");
}

View File

@ -20,9 +20,9 @@ final class ExplicitlyAnnotatedUnittestCheck : BaseAnalyzer
mixin AnalyzerInfo!"explicitly_annotated_unittests";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const Declaration decl)
@ -44,7 +44,14 @@ final class ExplicitlyAnnotatedUnittestCheck : BaseAnalyzer
}
}
if (!isSafeOrSystem)
addErrorMessage(decl.unittest_.findTokenForDisplay(tok!"unittest"), KEY, MESSAGE);
{
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);
}
@ -55,10 +62,10 @@ final class ExplicitlyAnnotatedUnittestCheck : BaseAnalyzer
unittest
{
import std.stdio : stderr;
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 : assertAnalyzerWarnings;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.explicitly_annotated_unittests = Check.enabled;
@ -94,5 +101,27 @@ unittest
ExplicitlyAnnotatedUnittestCheck.MESSAGE,
), sac);
// nested
assertAutoFix(q{
unittest {} // fix:0
pure nothrow @nogc unittest {} // fix:0
struct Foo
{
unittest {} // fix:1
pure nothrow @nogc unittest {} // fix:1
}
}c, q{
@safe unittest {} // fix:0
pure nothrow @nogc @safe unittest {} // fix:0
struct Foo
{
@system unittest {} // fix:1
pure nothrow @nogc @system unittest {} // fix:1
}
}c, sac);
stderr.writeln("Unittest for ExplicitlyAnnotatedUnittestCheck passed.");
}

View File

@ -57,7 +57,8 @@ private:
void addError(T)(const Token finalToken, T t, string msg)
{
import std.format : format;
addErrorMessage(finalToken.type ? finalToken : t.name, KEY, MSGB.format(msg));
addErrorMessage(finalToken.type ? finalToken : t.name, KEY, MSGB.format(msg),
[AutoFix.replacement(finalToken, "")]);
}
public:
@ -73,9 +74,9 @@ public:
};
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const(StructDeclaration) sd)
@ -253,10 +254,10 @@ public:
@system unittest
{
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings;
import std.stdio : stderr;
import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
import std.format : format;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.final_attribute_check = Check.enabled;
@ -432,5 +433,62 @@ public:
}
}, sac);
assertAutoFix(q{
final void foo(){} // fix
void foo(){final void foo(){}} // fix
void foo()
{
static if (true)
final class A{ private: final protected void foo(){}} // fix
}
final struct Foo{} // fix
final union Foo{} // fix
class Foo{private final void foo(){}} // fix
class Foo{private: final void foo(){}} // fix
interface Foo{final void foo(T)(){}} // fix
final class Foo{final void foo(){}} // fix
private: final class Foo {public: private final void foo(){}} // fix
class Foo {final static void foo(){}} // fix
class Foo
{
void foo(){}
static: final void foo(){} // fix
}
class Foo
{
void foo(){}
static{ final void foo(){}} // fix
void foo(){}
}
}, q{
void foo(){} // fix
void foo(){ void foo(){}} // fix
void foo()
{
static if (true)
final class A{ private: protected void foo(){}} // fix
}
struct Foo{} // fix
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
private: final class Foo {public: private void foo(){}} // fix
class Foo { static void foo(){}} // fix
class Foo
{
void foo(){}
static: void foo(){} // fix
}
class Foo
{
void foo(){}
static{ void foo(){}} // fix
void foo(){}
}
}, sac);
stderr.writeln("Unittest for FinalAttributeChecker passed.");
}

View File

@ -22,9 +22,9 @@ final class FloatOperatorCheck : BaseAnalyzer
enum string KEY = "dscanner.deprecated.floating_point_operators";
mixin AnalyzerInfo!"float_operator_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const RelExpression r)

View File

@ -28,9 +28,9 @@ final class FunctionAttributeCheck : BaseAnalyzer
mixin AnalyzerInfo!"function_attribute_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const InterfaceDeclaration dec)
@ -104,9 +104,15 @@ final class FunctionAttributeCheck : BaseAnalyzer
}
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'.");
~ " marked 'const', 'inout', or 'immutable'.", autofixes);
}
}
dec.accept(this);
@ -123,7 +129,8 @@ final class FunctionAttributeCheck : BaseAnalyzer
continue;
if (attr.attribute == tok!"abstract" && inInterface)
{
addErrorMessage(attr.attribute, KEY, ABSTRACT_MESSAGE);
addErrorMessage(attr.attribute, KEY, ABSTRACT_MESSAGE,
[AutoFix.replacement(attr.attribute, "")]);
continue;
}
if (attr.attribute == tok!"static")
@ -136,9 +143,21 @@ final class FunctionAttributeCheck : BaseAnalyzer
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));
"'%s' is not an attribute of the return type."
~ " Place it after the parameter list to clarify.",
attrString), autofixes);
}
}
end:
@ -204,5 +223,58 @@ unittest
}
}c, sac);
assertAutoFix(q{
int foo() @property { return 0; }
class ClassName {
const int confusingConst() { return 0; } // fix:0
const int confusingConst() { return 0; } // fix:1
int bar() @property { return 0; } // fix:0
int bar() @property { return 0; } // fix:1
int bar() @property { return 0; } // fix:2
}
struct StructName {
int bar() @property { return 0; } // fix:0
}
union UnionName {
int bar() @property { return 0; } // fix:0
}
interface InterfaceName {
int bar() @property; // fix:0
abstract int method(); // fix
}
}c, q{
int foo() @property { return 0; }
class ClassName {
int confusingConst() const { return 0; } // fix:0
const(int) confusingConst() { return 0; } // fix:1
int bar() const @property { return 0; } // fix:0
int bar() inout @property { return 0; } // fix:1
int bar() immutable @property { return 0; } // fix:2
}
struct StructName {
int bar() const @property { return 0; } // fix:0
}
union UnionName {
int bar() const @property { return 0; } // fix:0
}
interface InterfaceName {
int bar() const @property; // fix:0
int method(); // fix
}
}c, sac);
stderr.writeln("Unittest for FunctionAttributeCheck passed.");
}

View File

@ -22,9 +22,9 @@ final class HasPublicExampleCheck : BaseAnalyzer
mixin AnalyzerInfo!"has_public_example";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Module mod)
@ -88,6 +88,8 @@ final class HasPublicExampleCheck : BaseAnalyzer
private:
enum string KEY = "dscanner.style.has_public_example";
bool hasDitto(Decl)(const Decl decl)
{
import ddoc.comments : parseComment;
@ -164,7 +166,7 @@ private:
{
import std.string : format;
addErrorMessage(tokens, "dscanner.style.has_public_example", name is null
addErrorMessage(tokens, KEY, name is null
? "Public declaration has no documented example."
: format("Public declaration '%s' has no documented example.", name));
}

View File

@ -6,18 +6,19 @@
module dscanner.analysis.helpers;
import core.exception : AssertError;
import std.stdio;
import std.string;
import std.traits;
import std.stdio;
import dparse.ast;
import dparse.lexer : tok, Token;
import dparse.rollback_allocator;
import dsymbol.modulecache : ModuleCache;
import dscanner.analysis.base;
import dscanner.analysis.config;
import dscanner.analysis.run;
import dscanner.analysis.base;
import std.experimental.allocator.mallocator;
import dsymbol.modulecache : ModuleCache;
import std.experimental.allocator;
import std.experimental.allocator.mallocator;
S between(S)(S value, S before, S after) if (isSomeString!S)
{
@ -40,6 +41,24 @@ 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
@ -194,3 +213,140 @@ void assertAnalyzerWarnings(string code, const StaticAnalysisConfig config,
throw new AssertError(message, file, line);
}
}
/// EOL inside this project, for tests
private static immutable fileEol = q{
};
/**
* This assert function will analyze the passed in code, get the warnings, and
* apply all specified autofixes all at once.
*
* Indicate which autofix to apply by adding a line comment at the end of the
* line with the following content: `// fix:0`, where 0 is the index which
* autofix to apply. There may only be one diagnostic on a line with this fix
* comment. Alternatively you can also just write `// fix` to apply the only
* 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__)
{
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;
StringCache cache = StringCache(StringCache.defaultBucketCount);
RollbackAllocator r;
const(Token)[] tokens;
const(Module) m = parseModule(file, cast(ubyte[]) before, &r, defaultErrorFormat, cache, false, tokens);
ModuleCache moduleCache;
// 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;
scope (failure)
{
if (toApply.length)
stderr.writefln("Would have applied these fixes:%(\n- %s%)",
toApply.map!"a[0].autofixes[a[1]].name");
else
stderr.writeln("Did not find any fixes at all up to this point.");
stderr.writeln("Found warnings on lines: ", rawWarnings[].map!(a
=> a.endLine == 0 ? 0 : a.endLine - 1 + line));
}
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;
}
auto fixComment = codeLines[rawLine - 1].findSplit("// fix");
if (fixComment[1].length)
{
applyLines ~= cast(int)rawLine - 1;
if (fixComment[2].startsWith(":"))
{
auto i = fixComment[2][1 .. $].to!int;
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);
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);
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);
}
AutoFix.CodeReplacement[] replacements;
foreach_reverse (pair; toApply)
{
Message message = pair[0];
AutoFix fix = message.autofixes[pair[1]];
replacements ~= fix.expectReplacements;
}
replacements.sort!"a.range[0] < b.range[0]";
improveAutoFixWhitespace(before, replacements);
string newCode = before;
foreach_reverse (replacement; replacements)
{
newCode = newCode[0 .. replacement.range[0]] ~ replacement.newText
~ newCode[replacement.range[1] .. $];
}
if (newCode != after)
{
bool onlyWhitespaceDiffers = newCode.replace("\t", "").replace(" ", "")
== after.replace("\t", "").replace(" ", "").replace("\r", "");
string formatDisplay(string code)
{
string ret = code.lineSplitter!(KeepTerminator.yes).map!(a => "\t" ~ a).join;
if (onlyWhitespaceDiffers)
ret = ret
.replace("\r", "\x1B[2m\\r\x1B[m")
.replace("\t", "\x1B[2m→ \x1B[m")
.replace(" ", "\x1B[2m⸱\x1B[m");
return ret;
}
throw new AssertError("Applying autofix didn't yield expected results. Expected:\n"
~ formatDisplay(after)
~ "\n\nActual:\n"
~ formatDisplay(newCode),
file, line);
}
}

View File

@ -20,9 +20,9 @@ final class IfConstraintsIndentCheck : BaseAnalyzer
mixin AnalyzerInfo!"if_constraints_indent";
///
this(string fileName, const(Token)[] tokens, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
// convert tokens to a list of token starting positions per line

View File

@ -16,9 +16,9 @@ final class IfStatementCheck : BaseAnalyzer
alias visit = BaseAnalyzer.visit;
mixin AnalyzerInfo!"redundant_if_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const IfStatement ifStatement)

View File

@ -26,9 +26,9 @@ final class IfElseSameCheck : BaseAnalyzer
mixin AnalyzerInfo!"if_else_same_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const IfStatement ifStatement)
@ -39,7 +39,7 @@ final class IfElseSameCheck : BaseAnalyzer
// extend 1 past, so we include the `else` token
tokens = (tokens.ptr - 1)[0 .. tokens.length + 1];
addErrorMessage(tokens,
"dscanner.bugs.if_else_same", "'Else' branch is identical to 'Then' branch.");
IF_ELSE_SAME_KEY, "'Else' branch is identical to 'Then' branch.");
}
ifStatement.accept(this);
}
@ -50,7 +50,7 @@ final class IfElseSameCheck : BaseAnalyzer
if (e !is null && assignExpression.operator == tok!"="
&& e.ternaryExpression == assignExpression.ternaryExpression)
{
addErrorMessage(assignExpression, "dscanner.bugs.self_assignment",
addErrorMessage(assignExpression, SELF_ASSIGNMENT_KEY,
"Left side of assignment operatior is identical to the right side.");
}
assignExpression.accept(this);
@ -62,7 +62,7 @@ final class IfElseSameCheck : BaseAnalyzer
&& andAndExpression.left == andAndExpression.right)
{
addErrorMessage(andAndExpression.right,
"dscanner.bugs.logic_operator_operands",
LOGIC_OPERATOR_OPERANDS_KEY,
"Left side of logical and is identical to right side.");
}
andAndExpression.accept(this);
@ -74,11 +74,17 @@ final class IfElseSameCheck : BaseAnalyzer
&& orOrExpression.left == orOrExpression.right)
{
addErrorMessage(orOrExpression.right,
"dscanner.bugs.logic_operator_operands",
LOGIC_OPERATOR_OPERANDS_KEY,
"Left side of logical or is identical to right side.");
}
orOrExpression.accept(this);
}
private:
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

View File

@ -20,9 +20,9 @@ final class ImportSortednessCheck : BaseAnalyzer
mixin AnalyzerInfo!"imports_sortedness";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
mixin ScopedVisit!Module;

View File

@ -22,9 +22,9 @@ final class IncorrectInfiniteRangeCheck : BaseAnalyzer
mixin AnalyzerInfo!"incorrect_infinite_range_check";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const StructBody structBody)

View File

@ -13,23 +13,15 @@ import dscanner.analysis.helpers;
/**
* Checks for labels and variables that have the same name.
*/
final class LabelVarNameCheck : BaseAnalyzer
final class LabelVarNameCheck : ScopedBaseAnalyzer
{
mixin AnalyzerInfo!"label_var_same_name_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
mixin ScopedVisit!Module;
mixin ScopedVisit!BlockStatement;
mixin ScopedVisit!StructBody;
mixin ScopedVisit!CaseStatement;
mixin ScopedVisit!ForStatement;
mixin ScopedVisit!IfStatement;
mixin ScopedVisit!TemplateDeclaration;
mixin AggregateVisit!ClassDeclaration;
mixin AggregateVisit!StructDeclaration;
mixin AggregateVisit!InterfaceDeclaration;
@ -64,10 +56,12 @@ final class LabelVarNameCheck : BaseAnalyzer
--conditionalDepth;
}
alias visit = BaseAnalyzer.visit;
alias visit = ScopedBaseAnalyzer.visit;
private:
enum string KEY = "dscanner.suspicious.label_var_same_name";
Thing[string][] stack;
template AggregateVisit(NodeType)
@ -80,16 +74,6 @@ private:
}
}
template ScopedVisit(NodeType)
{
override void visit(const NodeType n)
{
pushScope();
n.accept(this);
popScope();
}
}
void duplicateCheck(const Token name, bool fromLabel, bool isConditional)
{
import std.conv : to;
@ -106,7 +90,7 @@ private:
{
immutable thisKind = fromLabel ? "Label" : "Variable";
immutable otherKind = thing.isVar ? "variable" : "label";
addErrorMessage(name, "dscanner.suspicious.label_var_same_name",
addErrorMessage(name, KEY,
thisKind ~ " \"" ~ fqn ~ "\" has the same name as a "
~ otherKind ~ " defined on line " ~ to!string(thing.line) ~ ".");
}
@ -128,12 +112,12 @@ private:
return stack[$ - 1];
}
void pushScope()
protected override void pushScope()
{
stack.length++;
}
void popScope()
protected override void popScope()
{
stack.length--;
}
@ -278,6 +262,21 @@ unittest
struct a { int a; }
}
unittest
{
switch (1) {
case 1:
int x, c1;
break;
case 2:
int x, c2;
break;
default:
int x, def;
break;
}
}
}c, sac);
stderr.writeln("Unittest for LabelVarNameCheck passed.");
}

View File

@ -16,13 +16,15 @@ final class LambdaReturnCheck : BaseAnalyzer
mixin AnalyzerInfo!"lambda_return_check";
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const FunctionLiteralExpression fLit)
{
import std.algorithm : find;
auto fe = safeAccess(fLit).assignExpression.as!UnaryExpression
.primaryExpression.functionLiteralExpression.unwrap;
@ -35,7 +37,21 @@ final class LambdaReturnCheck : BaseAnalyzer
auto endIncl = &fe.specifiedFunctionBody.tokens[0];
assert(endIncl >= start);
auto tokens = start[0 .. endIncl - start + 1];
addErrorMessage(tokens, KEY, "This lambda returns a lambda. Add parenthesis to clarify.");
auto arrow = tokens.find!(a => a.type == tok!"=>");
AutoFix[] autofixes;
if (arrow.length)
{
if (fLit.tokens[0] == tok!"(")
autofixes ~= AutoFix.replacement(arrow[0], "", "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], ""));
}
autofixes ~= AutoFix.insertionBefore(*endIncl, "() ", "Add parenthesis (return delegate)");
addErrorMessage(tokens, KEY, "This lambda returns a lambda. Add parenthesis to clarify.",
autofixes);
}
private:
@ -45,14 +61,14 @@ private:
version(Windows) {/*because of newline in code*/} else
unittest
{
import dscanner.analysis.helpers : assertAnalyzerWarnings;
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
sac.lambda_return_check = Check.enabled;
auto code = `
assertAnalyzerWarnings(q{
void main()
{
int[] b;
@ -64,7 +80,33 @@ unittest
^^^^^^^^ [warn]: This lambda returns a lambda. Add parenthesis to clarify. +/
pragma(msg, typeof({ return a; }));
pragma(msg, typeof(a => () { return a; }));
}`c;
assertAnalyzerWarnings(code, sac);
}
}c, sac);
assertAutoFix(q{
void main()
{
int[] b;
auto a = b.map!(a => { return a * a + 2; }).array(); // fix:0
auto a = b.map!(a => { return a * a + 2; }).array(); // fix:1
pragma(msg, typeof(a => { return a; })); // fix:0
pragma(msg, typeof(a => { return a; })); // fix:1
pragma(msg, typeof((a) => { return a; })); // fix:0
pragma(msg, typeof((a) => { return a; })); // fix:1
}
}c, q{
void main()
{
int[] b;
auto a = b.map!((a) { return a * a + 2; }).array(); // fix:0
auto a = b.map!(a => () { return a * a + 2; }).array(); // fix:1
pragma(msg, typeof((a) { return a; })); // fix:0
pragma(msg, typeof(a => () { return a; })); // fix:1
pragma(msg, typeof((a) { return a; })); // fix:0
pragma(msg, typeof((a) => () { return a; })); // fix:1
}
}c, sac);
stderr.writeln("Unittest for LambdaReturnCheck passed.");
}

View File

@ -18,13 +18,15 @@ import dsymbol.scope_;
*/
final class LengthSubtractionCheck : BaseAnalyzer
{
private enum string KEY = "dscanner.suspicious.length_subtraction";
alias visit = BaseAnalyzer.visit;
mixin AnalyzerInfo!"length_subtraction_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const AddExpression addExpression)
@ -40,8 +42,11 @@ final class LengthSubtractionCheck : BaseAnalyzer
if (l.identifierOrTemplateInstance is null
|| l.identifierOrTemplateInstance.identifier.text != "length")
goto end;
addErrorMessage(addExpression, "dscanner.suspicious.length_subtraction",
"Avoid subtracting from '.length' as it may be unsigned.");
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);
@ -62,5 +67,19 @@ unittest
writeln("something");
}
}c, sac);
assertAutoFix(q{
void testSizeT()
{
if (i < a.length - 1) // fix
writeln("something");
}
}c, q{
void testSizeT()
{
if (i < cast(ptrdiff_t) a.length - 1) // fix
writeln("something");
}
}c, sac);
stderr.writeln("Unittest for IfElseSameCheck passed.");
}

View File

@ -20,10 +20,9 @@ final class LineLengthCheck : BaseAnalyzer
mixin AnalyzerInfo!"long_line_check";
///
this(string fileName, const(Token)[] tokens, int maxLineLength, bool skipTests = false)
this(BaseAnalyzerArguments args, int maxLineLength)
{
super(fileName, null, skipTests);
this.tokens = tokens;
super(args);
this.maxLineLength = maxLineLength;
}
@ -94,9 +93,9 @@ private:
unittest
{
assert(new LineLengthCheck(null, null, 120).checkMultiLineToken(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 8);
assert(new LineLengthCheck(null, null, 120).checkMultiLineToken(Token(tok!"stringLiteral", " \na", 0, 0, 0)) == 2);
assert(new LineLengthCheck(null, null, 120).checkMultiLineToken(Token(tok!"stringLiteral", " \n ", 0, 0, 0)) == 5);
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)
@ -165,7 +164,6 @@ private:
enum string KEY = "dscanner.style.long_line";
const int maxLineLength;
const(Token)[] tokens;
}
@system unittest

View File

@ -25,9 +25,9 @@ final class LocalImportCheck : BaseAnalyzer
/**
* Construct with the given file name.
*/
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
mixin visitThing!StructBody;
@ -59,7 +59,7 @@ final class LocalImportCheck : BaseAnalyzer
if (singleImport.rename.text.length == 0)
{
addErrorMessage(singleImport,
"dscanner.suspicious.local_imports", "Local imports should specify"
KEY, "Local imports should specify"
~ " the symbols being imported to avoid hiding local symbols.");
}
}
@ -68,6 +68,8 @@ final class LocalImportCheck : BaseAnalyzer
private:
enum string KEY = "dscanner.suspicious.local_imports";
mixin template visitThing(T)
{
override void visit(const T thing)

View File

@ -26,9 +26,9 @@ final class LogicPrecedenceCheck : BaseAnalyzer
enum string KEY = "dscanner.confusing.logical_precedence";
mixin AnalyzerInfo!"logical_precedence_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const OrOrExpression orOr)

View File

@ -14,9 +14,9 @@ final class MismatchedArgumentCheck : BaseAnalyzer
mixin AnalyzerInfo!"mismatched_args_check";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const FunctionCallExpression fce)

View File

@ -0,0 +1,271 @@
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."))();
}

View File

@ -26,9 +26,9 @@ public:
/**
* Constructs the style checker with the given file name.
*/
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Token t)
@ -39,12 +39,15 @@ public:
&& ((t.text.startsWith("0b") && !t.text.matchFirst(badBinaryRegex)
.empty) || !t.text.matchFirst(badDecimalRegex).empty))
{
addErrorMessage(t, "dscanner.style.number_literals",
addErrorMessage(t, KEY,
"Use underscores to improve number constant readability.");
}
}
private:
enum string KEY = "dscanner.style.number_literals";
auto badBinaryRegex = ctRegex!(`^0b[01]{9,}`);
auto badDecimalRegex = ctRegex!(`^\d{5,}`);
}

View File

@ -24,9 +24,9 @@ final class ObjectConstCheck : BaseAnalyzer
mixin AnalyzerInfo!"object_const_check";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
mixin visitTemplate!ClassDeclaration;
@ -68,7 +68,7 @@ final class ObjectConstCheck : BaseAnalyzer
if (inAggregate && !constColon && !constBlock && !isDeclationDisabled
&& isInteresting(fd.name.text) && !hasConst(fd.memberFunctionAttributes))
{
addErrorMessage(d.functionDeclaration.name, "dscanner.suspicious.object_const",
addErrorMessage(d.functionDeclaration.name, KEY,
"Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.");
}
}
@ -81,7 +81,11 @@ final class ObjectConstCheck : BaseAnalyzer
constBlock = false;
}
private static bool hasConst(const MemberFunctionAttribute[] attributes)
private:
enum string KEY = "dscanner.suspicious.object_const";
static bool hasConst(const MemberFunctionAttribute[] attributes)
{
import std.algorithm : any;
@ -89,15 +93,14 @@ final class ObjectConstCheck : BaseAnalyzer
|| a.tokenType == tok!"immutable" || a.tokenType == tok!"inout");
}
private static bool isInteresting(string name)
static bool isInteresting(string name)
{
return name == "opCmp" || name == "toHash" || name == "opEquals"
|| name == "toString" || name == "opCast";
}
private bool constBlock;
private bool constColon;
bool constBlock;
bool constColon;
}
unittest

View File

@ -23,9 +23,9 @@ final class OpEqualsWithoutToHashCheck : BaseAnalyzer
mixin AnalyzerInfo!"opequals_tohash_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const ClassDeclaration node)

View File

@ -31,9 +31,9 @@ final class PokemonExceptionCheck : BaseAnalyzer
alias visit = BaseAnalyzer.visit;
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const LastCatch lc)

View File

@ -41,9 +41,9 @@ final class ProperlyDocumentedPublicFunctions : BaseAnalyzer
mixin AnalyzerInfo!"properly_documented_public_functions";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const Module mod)

View File

@ -29,9 +29,9 @@ final class BackwardsRangeCheck : BaseAnalyzer
* Params:
* fileName = the name of the file being analyzed
*/
this(string fileName, const Scope* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const ForeachStatement foreachStatement)

View File

@ -17,13 +17,13 @@ import std.range : empty, front, walkLength;
/**
* Checks for redundant attributes. At the moment only visibility attributes.
*/
final class RedundantAttributesCheck : BaseAnalyzer
final class RedundantAttributesCheck : ScopedBaseAnalyzer
{
mixin AnalyzerInfo!"redundant_attributes_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
stack.length = 0;
}
@ -67,15 +67,8 @@ final class RedundantAttributesCheck : BaseAnalyzer
}
}
alias visit = BaseAnalyzer.visit;
alias visit = ScopedBaseAnalyzer.visit;
mixin ScopedVisit!Module;
mixin ScopedVisit!BlockStatement;
mixin ScopedVisit!StructBody;
mixin ScopedVisit!CaseStatement;
mixin ScopedVisit!ForStatement;
mixin ScopedVisit!IfStatement;
mixin ScopedVisit!TemplateDeclaration;
mixin ScopedVisit!ConditionalDeclaration;
private:
@ -153,22 +146,12 @@ private:
return currentAttributes.map!(a => a.attribute.type.str).joiner(",").to!string;
}
template ScopedVisit(NodeType)
{
override void visit(const NodeType n)
{
pushScope();
n.accept(this);
popScope();
}
}
void pushScope()
protected override void pushScope()
{
stack.length++;
}
void popScope()
protected override void popScope()
{
stack.length--;
}

View File

@ -20,9 +20,9 @@ final class RedundantParenCheck : BaseAnalyzer
mixin AnalyzerInfo!"redundant_parens_check";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const IfStatement statement)

View File

@ -22,9 +22,9 @@ final class RedundantStorageClassCheck : BaseAnalyzer
enum string REDUNDANT_VARIABLE_ATTRIBUTES = "Variable declaration for `%s` has redundant attributes (%-(`%s`%|, %)).";
mixin AnalyzerInfo!"redundant_storage_classes";
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const Declaration node)
@ -59,9 +59,11 @@ final class RedundantStorageClassCheck : BaseAnalyzer
return;
auto t = vd.declarators[0].name;
string message = REDUNDANT_VARIABLE_ATTRIBUTES.format(t.text, globalAttributes);
addErrorMessage(t, "dscanner.unnecessary.duplicate_attribute", message);
addErrorMessage(t, KEY, message);
}
}
private enum string KEY = "dscanner.unnecessary.duplicate_attribute";
}
unittest

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ import dscanner.utils : safeAccess;
* } else if (bar) {
* }
* ---
*
*
* However, it's more likely that this is a mistake.
*/
final class StaticIfElse : BaseAnalyzer
@ -28,9 +28,9 @@ final class StaticIfElse : BaseAnalyzer
mixin AnalyzerInfo!"static_if_else_check";
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const ConditionalStatement cc)
@ -48,7 +48,11 @@ final class StaticIfElse : BaseAnalyzer
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.");
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])
]);
}
const(IfStatement) getIfStatement(const ConditionalStatement cc)
@ -56,13 +60,40 @@ final class StaticIfElse : BaseAnalyzer
return safeAccess(cc).falseStatement.statement.statementNoCaseNoDefault.ifStatement;
}
override AutoFix.CodeReplacement[] resolveAutoFix(
const Module mod,
scope const(Token)[] tokens,
const AutoFix.ResolveContext context,
const AutoFixFormatting formatting,
)
{
import dscanner.analysis.helpers : getLineIndentation;
import std.algorithm : countUntil;
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");
auto indentation = getLineIndentation(tokens, tokens[beforeElse].line, formatting);
string beforeIf = formatting.getWhitespaceBeforeOpeningBrace(indentation, false)
~ "{" ~ formatting.eol ~ indentation;
string afterIf = formatting.eol ~ indentation ~ "}";
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;
}
enum KEY = "dscanner.suspicious.static_if_else";
}
unittest
{
import dscanner.analysis.helpers : assertAnalyzerWarnings;
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
import dscanner.analysis.config : Check, disabledConfig, StaticAnalysisConfig;
import dscanner.analysis.helpers : assertAnalyzerWarnings, assertAutoFix;
import std.stdio : stderr;
StaticAnalysisConfig sac = disabledConfig();
@ -88,5 +119,51 @@ unittest
}
}c, sac);
assertAutoFix(q{
void foo() {
static if (false)
auto a = 0;
else if (true) // fix:0
auto b = 1;
}
void bar() {
static if (false)
auto a = 0;
else if (true) // fix:1
auto b = 1;
}
void baz() {
static if (false)
auto a = 0;
else if (true) { // fix:1
auto b = 1;
}
}
}c, q{
void foo() {
static if (false)
auto a = 0;
else static if (true) // fix:0
auto b = 1;
}
void bar() {
static if (false)
auto a = 0;
else {
if (true) // fix:1
auto b = 1;
}
}
void baz() {
static if (false)
auto a = 0;
else {
if (true) { // fix:1
auto b = 1;
}
}
}
}c, sac);
stderr.writeln("Unittest for StaticIfElse passed.");
}

View File

@ -13,9 +13,10 @@ final class StatsCollector : BaseAnalyzer
{
alias visit = ASTVisitor.visit;
this(string fileName)
this(BaseAnalyzerArguments args)
{
super(fileName, null);
args.skipTests = false; // old behavior compatibility
super(args);
}
override void visit(const Statement statement)

View File

@ -14,6 +14,7 @@ import std.conv;
import std.format;
import dscanner.analysis.helpers;
import dscanner.analysis.base;
import dscanner.analysis.nolint;
import dsymbol.scope_ : Scope;
final class StyleChecker : BaseAnalyzer
@ -26,13 +27,16 @@ final class StyleChecker : BaseAnalyzer
enum string KEY = "dscanner.style.phobos_naming_convention";
mixin AnalyzerInfo!"style_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const ModuleDeclaration dec)
{
with (noLint.push(NoLintFactory.fromModuleDeclaration(dec)))
dec.accept(this);
foreach (part; dec.moduleName.identifiers)
{
if (part.text.matchFirst(moduleNameRegex).length == 0)

View File

@ -31,9 +31,9 @@ public:
mixin AnalyzerInfo!"trust_too_much";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const AtAttribute d)

View File

@ -23,9 +23,9 @@ final class UndocumentedDeclarationCheck : BaseAnalyzer
mixin AnalyzerInfo!"undocumented_declaration_check";
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Module mod)
@ -146,6 +146,8 @@ final class UndocumentedDeclarationCheck : BaseAnalyzer
private:
enum string KEY = "dscanner.style.undocumented_declaration";
mixin template V(T)
{
override void visit(const T declaration)
@ -223,7 +225,7 @@ private:
{
import std.string : format;
addErrorMessage(range, "dscanner.style.undocumented_declaration", name is null
addErrorMessage(range, KEY, name is null
? "Public declaration is undocumented."
: format("Public declaration '%s' is undocumented.", name));
}

View File

@ -5,6 +5,7 @@
module dscanner.analysis.unmodified;
import dscanner.analysis.base;
import dscanner.analysis.nolint;
import dscanner.utils : safeAccess;
import dsymbol.scope_ : Scope;
import std.container;
@ -21,9 +22,9 @@ final class UnmodifiedFinder : BaseAnalyzer
mixin AnalyzerInfo!"could_be_immutable_check";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Module mod)
@ -114,11 +115,15 @@ final class UnmodifiedFinder : BaseAnalyzer
if (canFindImmutableOrConst(dec))
{
isImmutable++;
dec.accept(this);
with (noLint.push(NoLintFactory.fromDeclaration(dec)))
dec.accept(this);
isImmutable--;
}
else
dec.accept(this);
{
with (noLint.push(NoLintFactory.fromDeclaration(dec)))
dec.accept(this);
}
}
override void visit(const IdentifierChain ic)
@ -189,6 +194,8 @@ final class UnmodifiedFinder : BaseAnalyzer
private:
enum string KEY = "dscanner.suspicious.unmodified";
template PartsMightModify(T)
{
override void visit(const T t)
@ -300,7 +307,7 @@ private:
{
immutable string errorMessage = "Variable " ~ vi.name
~ " is never modified and could have been declared const or immutable.";
addErrorMessage(vi.token, "dscanner.suspicious.unmodified", errorMessage);
addErrorMessage(vi.token, KEY, errorMessage);
}
tree = tree[0 .. $ - 1];
}
@ -379,5 +386,12 @@ bool isValueTypeSimple(const Type type) pure nothrow @nogc
foo(i2);
}
}, sac);
assertAnalyzerWarnings(q{
@("nolint(dscanner.suspicious.unmodified)")
void foo(){
int i = 1;
}
}, sac);
}

View File

@ -20,12 +20,10 @@ abstract class UnusedIdentifierCheck : BaseAnalyzer
alias visit = BaseAnalyzer.visit;
/**
* Params:
* fileName = the name of the file being analyzed
*/
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
re = regex("[\\p{Alphabetic}_][\\w_]*");
}
@ -79,6 +77,13 @@ abstract class UnusedIdentifierCheck : BaseAnalyzer
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)
@ -414,15 +419,13 @@ abstract class UnusedStorageCheck : UnusedIdentifierCheck
/**
* Params:
* fileName = the name of the file being analyzed
* sc = the scope
* skipTest = whether tests should be analyzed
* publicType = declaration kind used in error messages, e.g. "Variable"s
* reportType = declaration kind used in error reports, e.g. "unused_variable"
* 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(string fileName, const(Scope)* sc, bool skipTests = false, string publicType = null, string reportType = null)
this(BaseAnalyzerArguments args, string publicType = null, string reportType = null)
{
super(fileName, sc, skipTests);
super(args);
this.publicType = publicType;
this.reportType = reportType;
}

View File

@ -21,9 +21,9 @@ final class UnusedLabelCheck : BaseAnalyzer
mixin AnalyzerInfo!"unused_label_check";
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
}
override void visit(const Module mod)
@ -115,6 +115,8 @@ final class UnusedLabelCheck : BaseAnalyzer
private:
enum string KEY = "dscanner.suspicious.unused_label";
static struct Label
{
string name;
@ -144,7 +146,7 @@ private:
}
else if (!label.used)
{
addErrorMessage(label.token, "dscanner.suspicious.unused_label",
addErrorMessage(label.token, KEY,
"Label \"" ~ label.name ~ "\" is not used.");
}
}

View File

@ -23,9 +23,9 @@ final class UnusedParameterCheck : UnusedStorageCheck
* Params:
* fileName = the name of the file being analyzed
*/
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests, "Parameter", "unused_parameter");
super(args, "Parameter", "unused_parameter");
}
override void visit(const Parameter parameter)

View File

@ -41,9 +41,9 @@ public:
const(DSymbol)* noreturn_;
///
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests);
super(args);
void_ = sc.getSymbolsByName(internString("void"))[0];
auto symbols = sc.getSymbolsByName(internString("noreturn"));
if (symbols.length > 0)

View File

@ -23,9 +23,9 @@ final class UnusedVariableCheck : UnusedStorageCheck
* Params:
* fileName = the name of the file being analyzed
*/
this(string fileName, const(Scope)* sc, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, sc, skipTests, "Variable", "unused_variable");
super(args, "Variable", "unused_variable");
}
override void visit(const VariableDeclaration variableDeclaration)
@ -125,6 +125,12 @@ final class UnusedVariableCheck : UnusedStorageCheck
__traits(isPOD);
}
void unitthreaded()
{
auto testVar = foo.sort!myComp;
genVar.should == testVar;
}
}c, sac);
stderr.writeln("Unittest for UnusedVariableCheck passed.");
}

View File

@ -30,9 +30,9 @@ final class UselessAssertCheck : BaseAnalyzer
mixin AnalyzerInfo!"useless_assert_check";
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const AssertExpression ae)

View File

@ -5,6 +5,7 @@
module dscanner.analysis.useless_initializer;
import dscanner.analysis.base;
import dscanner.analysis.nolint;
import dscanner.utils : safeAccess;
import containers.dynamicarray;
import containers.hashmap;
@ -33,7 +34,7 @@ final class UselessInitializerChecker : BaseAnalyzer
private:
enum key = "dscanner.useless-initializer";
enum string KEY = "dscanner.useless-initializer";
version(unittest)
{
@ -55,9 +56,9 @@ private:
public:
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
_inStruct.insert(false);
}
@ -92,7 +93,10 @@ public:
override void visit(const(Declaration) decl)
{
_inStruct.insert(decl.structDeclaration !is null);
decl.accept(this);
with (noLint.push(NoLintFactory.fromDeclaration(decl)))
decl.accept(this);
if (_inStruct.length > 1 && _inStruct[$-2] && decl.constructor &&
((decl.constructor.parameters && decl.constructor.parameters.parameters.length == 0) ||
!decl.constructor.parameters))
@ -157,7 +161,7 @@ public:
{
void warn(const BaseNode range)
{
addErrorMessage(range, key, msg);
addErrorMessage(range, KEY, msg);
}
}
else
@ -165,7 +169,7 @@ public:
import std.format : format;
void warn(const BaseNode range)
{
addErrorMessage(range, key, msg.format(declarator.name.text));
addErrorMessage(range, KEY, msg.format(declarator.name.text));
}
}
@ -361,6 +365,45 @@ public:
NotKnown nk = NotKnown.init;
}, sac);
// passes
assertAnalyzerWarnings(q{
@("nolint(dscanner.useless-initializer)")
int a = 0;
int a = 0; /+
^ [warn]: X +/
@("nolint(dscanner.useless-initializer)")
int f() {
int a = 0;
}
struct nolint { string s; }
@nolint("dscanner.useless-initializer")
int a = 0;
int a = 0; /+
^ [warn]: X +/
@("nolint(other_check, dscanner.useless-initializer, another_one)")
int a = 0;
@nolint("other_check", "another_one", "dscanner.useless-initializer")
int a = 0;
}, sac);
// passes (disable check at module level)
assertAnalyzerWarnings(q{
@("nolint(dscanner.useless-initializer)")
module my_module;
int a = 0;
int f() {
int a = 0;
}
}, sac);
stderr.writeln("Unittest for UselessInitializerChecker passed.");
}

View File

@ -145,9 +145,9 @@ private:
public:
///
this(string fileName, bool skipTests = false)
this(BaseAnalyzerArguments args)
{
super(fileName, null, skipTests);
super(args);
}
override void visit(const(ClassDeclaration) decl)

View File

@ -10,8 +10,10 @@ import std.array;
import dparse.lexer;
// http://ethanschoonover.com/solarized
void highlight(R)(ref R tokens, string fileName)
void highlight(R)(ref R tokens, string fileName, string themeName)
{
immutable(Theme)* theme = getTheme(themeName);
stdout.writeln(q"[
<!DOCTYPE html>
<html>
@ -20,17 +22,19 @@ void highlight(R)(ref R tokens, string fileName)
stdout.writeln("<title>", fileName, "</title>");
stdout.writeln(q"[</head>
<body>
<style type="text/css">
html { background-color: #fdf6e3; color: #002b36; }
.kwrd { color: #b58900; font-weight: bold; }
.com { color: #93a1a1; font-style: italic; }
.num { color: #dc322f; font-weight: bold; }
.str { color: #2aa198; font-style: italic; }
.op { color: #586e75; font-weight: bold; }
.type { color: #268bd2; font-weight: bold; }
.cons { color: #859900; font-weight: bold; }
<style type="text/css">]");
stdout.writefln("
html { background-color: %s; color: %s; }
.kwrd { color: %s; font-weight: bold; }
.com { color: %s; font-style: italic; }
.num { color: %s; font-weight: bold; }
.str { color: %s; font-style: italic; }
.op { color: %s; font-weight: bold; }
.type { color: %s; font-weight: bold; }
.cons { color: %s; font-weight: bold; }
</style>
<pre>]");
<pre>", theme.bg, theme.fg, theme.kwrd, theme.com, theme.num, theme.str,
theme.op, theme.type, theme.cons);
while (!tokens.empty)
{
@ -76,3 +80,37 @@ void writeSpan(string cssClass, string value)
stdout.write(`<span class="`, cssClass, `">`, value.replace("&",
"&amp;").replace("<", "&lt;"), `</span>`);
}
struct Theme
{
string bg;
string fg;
string kwrd;
string com;
string num;
string str;
string op;
string type;
string cons;
}
immutable(Theme)* getTheme(string themeName)
{
immutable Theme[string] themes = [
"solarized": Theme("#fdf6e3", "#002b36", "#b58900", "#93a1a1", "#dc322f", "#2aa198", "#586e75",
"#268bd2", "#859900"),
"solarized-dark": Theme("#002b36", "#fdf6e3", "#b58900", "#586e75", "#dc322f", "#2aa198",
"#93a1a1", "#268bd2", "#859900"),
"gruvbox": Theme("#fbf1c7", "#282828", "#b57614", "#a89984", "#9d0006", "#427b58",
"#504945", "#076678", "#79740e"),
"gruvbox-dark": Theme("#282828", "#fbf1c7", "#d79921", "#7c6f64",
"#cc241d", "#689d6a", "#a89984", "#458588", "#98971a")
];
immutable(Theme)* theme = themeName in themes;
// Default theme
if (theme is null)
theme = &themes["solarized"];
return theme;
}

View File

@ -5,20 +5,21 @@
module dscanner.main;
import std.algorithm;
import std.array;
import std.conv;
import std.file;
import std.getopt;
import std.path;
import std.stdio;
import std.range;
import std.experimental.lexer;
import std.typecons : scoped;
import std.functional : toDelegate;
import dparse.lexer;
import dparse.parser;
import dparse.rollback_allocator;
import std.algorithm;
import std.array;
import std.conv;
import std.experimental.lexer;
import std.file;
import std.functional : toDelegate;
import std.getopt;
import std.path;
import std.range;
import std.stdio;
import std.string : chomp, splitLines;
import std.typecons : scoped;
import dscanner.highlighter;
import dscanner.stats;
@ -44,6 +45,7 @@ version (unittest)
else
int main(string[] args)
{
bool autofix;
bool sloc;
bool highlight;
bool ctags;
@ -62,22 +64,30 @@ else
bool defaultConfig;
bool report;
bool skipTests;
bool applySingleFixes;
string theme;
string resolveMessage;
string reportFormat;
string reportFile;
string symbolName;
string configLocation;
string[] importPaths;
string[] excludePaths;
bool printVersion;
bool explore;
bool verbose;
string errorFormat;
if (args.length == 2 && args[1].startsWith("@"))
args = args[0] ~ readText(args[1][1 .. $]).chomp.splitLines;
try
{
// dfmt off
getopt(args, std.getopt.config.caseSensitive,
"sloc|l", &sloc,
"highlight", &highlight,
"theme", &theme,
"ctags|c", &ctags,
"help|h", &help,
"etags|e", &etags,
@ -96,7 +106,10 @@ else
"report", &report,
"reportFormat", &reportFormat,
"reportFile", &reportFile,
"resolveMessage", &resolveMessage,
"applySingle", &applySingleFixes,
"I", &importPaths,
"exclude", &excludePaths,
"version", &printVersion,
"muffinButton", &muffin,
"explore", &explore,
@ -165,14 +178,45 @@ else
return 0;
}
if (args.length > 1 && args[1] == "lint")
if (args.length > 1)
{
args = args[0] ~ args[2 .. $];
styleCheck = true;
if (!errorFormat.length)
errorFormat = "pretty";
switch (args[1])
{
case "lint":
args = args[0] ~ args[2 .. $];
styleCheck = true;
if (!errorFormat.length)
errorFormat = "pretty";
break;
case "fix":
args = args[0] ~ args[2 .. $];
autofix = true;
if (!errorFormat.length)
errorFormat = "pretty";
break;
default:
break;
}
}
auto expandedArgs = () {
auto expanded = expandArgs(args);
if (excludePaths.length)
{
string[] newArgs = [expanded[0]];
foreach(arg; args[1 .. $])
{
if(!excludePaths.map!(p => arg.isSubpathOf(p))
.fold!((a, b) => a || b))
newArgs ~= arg;
}
return newArgs;
}
else
return expanded;
}();
if (!errorFormat.length)
errorFormat = defaultErrorFormat;
else if (auto errorFormatSuppl = errorFormat in errorFormatMap)
@ -184,8 +228,7 @@ else
.replace("\\n", "\n")
.replace("\\t", "\t");
const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath()
.buildNormalizedPath()).array();
const(string[]) absImportPaths = importPaths.map!absoluteNormalizedPath.array;
ModuleCache moduleCache;
@ -195,9 +238,11 @@ else
if (reportFormat.length || reportFile.length)
report = true;
immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount, syntaxCheck, ast, imports,
outline, tokenDump, styleCheck, defaultConfig, report,
symbolName !is null, etags, etagsAll, recursiveImports]);
immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount,
syntaxCheck, ast, imports, outline, tokenDump, styleCheck,
defaultConfig, report, autofix, resolveMessage.length,
symbolName !is null, etags, etagsAll, recursiveImports,
]);
if (optionCount > 1)
{
stderr.writeln("Too many options specified");
@ -233,7 +278,7 @@ else
if (highlight)
{
auto tokens = byToken(bytes, config, &cache);
dscanner.highlighter.highlight(tokens, args.length == 1 ? "stdin" : args[1]);
dscanner.highlighter.highlight(tokens, args.length == 1 ? "stdin" : args[1], theme);
return 0;
}
else if (tokenDump)
@ -258,17 +303,17 @@ else
}
else if (symbolName !is null)
{
stdout.findDeclarationOf(symbolName, expandArgs(args));
stdout.findDeclarationOf(symbolName, expandedArgs);
}
else if (ctags)
{
stdout.printCtags(expandArgs(args));
stdout.printCtags(expandedArgs);
}
else if (etags || etagsAll)
{
stdout.printEtags(etagsAll, expandArgs(args));
stdout.printEtags(etagsAll, expandedArgs);
}
else if (styleCheck)
else if (styleCheck || autofix || resolveMessage.length)
{
StaticAnalysisConfig config = defaultStaticAnalysisConfig();
string s = configLocation is null ? getConfigurationLocation() : configLocation;
@ -276,7 +321,17 @@ else
readINIFile(config, s);
if (skipTests)
config.enabled2SkipTests;
if (report)
if (autofix)
{
return .autofix(expandedArgs, config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0;
}
else if (resolveMessage.length)
{
listAutofixes(config, resolveMessage, usingStdin, usingStdin ? "stdin" : args[1], &cache, moduleCache);
return 0;
}
else if (report)
{
switch (reportFormat)
{
@ -285,19 +340,19 @@ else
goto case;
case "":
case "dscanner":
generateReport(expandArgs(args), config, cache, moduleCache, reportFile);
generateReport(expandedArgs, config, cache, moduleCache, reportFile);
break;
case "sonarQubeGenericIssueData":
generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache, reportFile);
generateSonarQubeGenericIssueDataReport(expandedArgs, config, cache, moduleCache, reportFile);
break;
}
}
else
return analyze(expandArgs(args), config, errorFormat, cache, moduleCache, true) ? 1 : 0;
return analyze(expandedArgs, config, errorFormat, cache, moduleCache, true) ? 1 : 0;
}
else if (syntaxCheck)
{
return .syntaxCheck(usingStdin ? ["stdin"] : expandArgs(args), errorFormat, cache, moduleCache) ? 1 : 0;
return .syntaxCheck(usingStdin ? ["stdin"] : expandedArgs, errorFormat, cache, moduleCache) ? 1 : 0;
}
else
{
@ -316,7 +371,7 @@ else
else
{
ulong count;
foreach (f; expandArgs(args))
foreach (f; expandedArgs)
{
LexerConfig config;
@ -363,12 +418,15 @@ else
void printHelp(string programName)
{
stderr.writefln(`
stdout.writefln(`
Usage: %1$s <options>
Human-readable output:
%1$s lint <options> <files...>
Interactively fixing issues
%1$s fix [--applySingle] <files...>
Parsable outputs:
%1$s -S <options> <files...>
%1$s --report <options> <files...>
@ -411,6 +469,9 @@ Options:
modules. This option can be passed multiple times to specify multiple
directories.
--exclude <file | directory>..., <file | directory>
Specify files or directories that will be ignored by D-Scanner.
--syntaxCheck <file>, -s <file>
Lexes and parses sourceFile, printing the line and column number of
any syntax errors to stdout. One error or warning is printed per line,
@ -435,6 +496,8 @@ Options:
- {endLine}: end line number, 1-based, inclusive
- {column}: start column on start line, 1-based, in bytes
- {endColumn}: end column on end line, 1-based, in bytes, exclusive
- {startIndex}: start file byte offset, 0-based
- {endIndex}: end file byte offset, 0-based
- {type}: "error" or "warn", uppercase variants: {Type}, {TYPE},
- {type2}: "error" or "warning", uppercase variants: {Type2}, {TYPE2}
- {message}: human readable message such as "Variable c is never used."
@ -494,7 +557,11 @@ Options:
--skipTests
Does not analyze code in unittests. Only works if --styleCheck
is specified.`,
is specified.
--applySingle
when running "dscanner fix", automatically apply all fixes that have
only one auto-fix.`,
programName, defaultErrorFormat, errorFormatMap);
}
@ -507,6 +574,9 @@ private enum CONFIG_FILE_NAME = "dscanner.ini";
version (linux) version = useXDG;
version (BSD) version = useXDG;
version (FreeBSD) version = useXDG;
version (OpenBSD) version = useXDG;
version (NetBSD) version = useXDG;
version (DragonflyBSD) version = useXDG;
version (OSX) version = useXDG;
/**

View File

@ -55,6 +55,9 @@ class DScannerJsonReporter
private static JSONValue toJson(Issue issue)
{
import std.sumtype : match;
import dscanner.analysis.base : AutoFix;
// dfmt off
JSONValue js = JSONValue([
"key": JSONValue(issue.message.key),
@ -80,6 +83,27 @@ class DScannerJsonReporter
"message": JSONValue(a.message),
])
).array
),
"autofixes": JSONValue(
issue.message.autofixes.map!(a =>
JSONValue([
"name": JSONValue(a.name),
"replacements": a.replacements.match!(
(const AutoFix.CodeReplacement[] replacements) => JSONValue(
replacements.map!(r => JSONValue([
"range": JSONValue([
JSONValue(r.range[0]),
JSONValue(r.range[1])
]),
"newText": JSONValue(r.newText)
])).array
),
(const AutoFix.ResolveContext _) => JSONValue(
"resolvable"
)
)
])
).array
)
]);
// dfmt on

View File

@ -6,6 +6,7 @@ import std.conv : to;
import std.encoding : BOM, BOMSeq, EncodingException, getBOM;
import std.format : format;
import std.file : exists, read;
import std.path: isValidPath;
private void processBOM(ref ubyte[] sourceCode, string fname)
{
@ -80,6 +81,19 @@ ubyte[] readFile(string fileName)
return sourceCode;
}
void writeFileSafe(string filename, scope const(ubyte)[] content)
{
import std.file : copy, PreserveAttributes, remove, write;
import std.path : baseName, buildPath, dirName;
string tempName = buildPath(filename.dirName, "." ~ filename.baseName ~ "~");
// FIXME: we are removing the optional BOM here
copy(filename, tempName, PreserveAttributes.yes);
write(filename, content);
remove(tempName);
}
string[] expandArgs(string[] args)
{
import std.file : isFile, FileException, dirEntries, SpanMode;
@ -115,12 +129,63 @@ string[] expandArgs(string[] args)
return rVal;
}
package string absoluteNormalizedPath(in string path)
{
import std.path: absolutePath, buildNormalizedPath;
return path.absolutePath().buildNormalizedPath();
}
private bool areSamePath(in string path1, in string path2)
in(path1.isValidPath && path2.isValidPath)
{
return path1.absoluteNormalizedPath() == path2.absoluteNormalizedPath();
}
unittest
{
assert(areSamePath("/abc/efg", "/abc/efg"));
assert(areSamePath("/abc/../abc/efg", "/abc/efg"));
assert(!areSamePath("/abc/../abc/../efg", "/abc/efg"));
}
package bool isSubpathOf(in string potentialSubPath, in string base)
in(base.isValidPath && potentialSubPath.isValidPath)
{
import std.path: isValidPath, relativePath;
import std.algorithm: canFind;
if(areSamePath(base, potentialSubPath))
return true;
const relative = relativePath(
potentialSubPath.absoluteNormalizedPath(),
base.absoluteNormalizedPath()
);
// No '..' in the relative paths means that potentialSubPath
// is actually a descendant of base
return !relative.canFind("..");
}
unittest
{
const base = "/abc/efg";
assert("/abc/efg/".isSubpathOf(base));
assert("/abc/efg/hij/".isSubpathOf(base));
assert("/abc/efg/hij/../kel".isSubpathOf(base));
assert(!"/abc/kel".isSubpathOf(base));
assert(!"/abc/efg/../kel".isSubpathOf(base));
}
/**
* Allows to build access chains of class members as done with the $(D ?.) operator
* in other languages. In the chain, any $(D null) member that is a class instance
* or that returns one, has for effect to shortcut the complete evaluation.
*
* This function is copied from https://github.com/BBasile/iz to avoid a new submodule.
* This function is copied from
* https://gitlab.com/basile.b/iz/-/blob/18f5c1e78a89edae9f7bd9c2d8e7e0c152f56696/import/iz/sugar.d#L1543
* to avoid adding additional dependencies.
* Any change made to this copy should also be applied to the origin.
*
* Params:

113
tests/dscanner.ini Normal file
View File

@ -0,0 +1,113 @@
; Configure which static analysis checks are enabled
[analysis.config.StaticAnalysisConfig]
; Check variable, class, struct, interface, union, and function names against the Phobos style guide
style_check="enabled"
; Check for array literals that cause unnecessary allocation
enum_array_literal_check="enabled"
; Check for poor exception handling practices
exception_check="enabled"
; Check for use of the deprecated 'delete' keyword
delete_check="enabled"
; Check for use of the deprecated floating point operators
float_operator_check="enabled"
; Check number literals for readability
number_style_check="enabled"
; Checks that opEquals, opCmp, toHash, and toString are either const, immutable, or inout.
object_const_check="enabled"
; Checks for .. expressions where the left side is larger than the right.
backwards_range_check="enabled"
; Checks for if statements whose 'then' block is the same as the 'else' block
if_else_same_check="enabled"
; Checks for some problems with constructors
constructor_check="enabled"
; Checks for unused variables
unused_variable_check="enabled"
; Checks for unused labels
unused_label_check="enabled"
; Checks for unused function parameters
unused_parameter_check="enabled"
; Checks for duplicate attributes
duplicate_attribute="enabled"
; Checks that opEquals and toHash are both defined or neither are defined
opequals_tohash_check="enabled"
; Checks for subtraction from .length properties
length_subtraction_check="enabled"
; Checks for methods or properties whose names conflict with built-in properties
builtin_property_names_check="enabled"
; Checks for confusing code in inline asm statements
asm_style_check="enabled"
; Checks for confusing logical operator precedence
logical_precedence_check="enabled"
; Checks for undocumented public declarations
undocumented_declaration_check="disabled"
; Checks for poor placement of function attributes
function_attribute_check="enabled"
; Checks for use of the comma operator
comma_expression_check="enabled"
; Checks for local imports that are too broad. Only accurate when checking code used with D versions older than 2.071.0
local_import_check="enabled"
; Checks for variables that could be declared immutable
could_be_immutable_check="enabled"
; Checks for redundant expressions in if statements
redundant_if_check="enabled"
; Checks for redundant parenthesis
redundant_parens_check="enabled"
; Checks for mismatched argument and parameter names
mismatched_args_check="enabled"
; Checks for labels with the same name as variables
label_var_same_name_check="enabled"
; Checks for lines longer than `max_line_length` characters
long_line_check="enabled"
; Checks for assignment to auto-ref function parameters
auto_ref_assignment_check="enabled"
; Checks for incorrect infinite range definitions
incorrect_infinite_range_check="enabled"
; Checks for asserts that are always true
useless_assert_check="enabled"
; Check for uses of the old-style alias syntax
alias_syntax_check="enabled"
; Checks for else if that should be else static if
static_if_else_check="enabled"
; Check for unclear lambda syntax
lambda_return_check="enabled"
; Check for auto function without return statement
auto_function_check="enabled"
; Check for sortedness of imports
imports_sortedness="enabled"
; Check for explicitly annotated unittests
explicitly_annotated_unittests="enabled"
; Check for properly documented public functions (Returns, Params)
properly_documented_public_functions="enabled"
; Check for useless usage of the final attribute
final_attribute_check="enabled"
; Check for virtual calls in the class constructors
vcall_in_ctor="enabled"
; Check for useless user defined initializers
useless_initializer="enabled"
; Check allman brace style
allman_braces_check="enabled"
; Check for redundant attributes
redundant_attributes_check="enabled"
; Check public declarations without a documented unittest
has_public_example="enabled"
; Check for asserts without an explanatory message
assert_without_msg="enabled"
; Check indent of if constraints
if_constraints_indent="enabled"
; Check for @trusted applied to a bigger scope than a single function
trust_too_much="enabled"
; Check for redundant storage classes on variable declarations
redundant_storage_classes="enabled"
; Check for unused function return values
unused_result="enabled"
; Enable cyclomatic complexity check
cyclomatic_complexity="enabled"
; Check for function bodies on discord functions
body_on_disabled_func_check="enabled"
; Formatting brace style for automatic fixes (allman, otbs, stroustrup, knr)
brace_style="allman"
; Formatting indentation style for automatic fixes (tabs, spaces)
indentation_style="tab"
; Formatting line ending character (lf, cr, crlf)
eol_style="lf"

82
tests/it.sh Executable file
View File

@ -0,0 +1,82 @@
#!/bin/bash
set -eu -o pipefail
function section {
e=$'\e'
if [ ! -z "${GITHUB_ACTION:-}" ]; then
echo "::endgroup::"
echo "::group::$@"
else
echo "$e[1m$@$e[m"
fi
}
function error {
echo $'\e[31;1mTests have failed.\e[m'
exit 1
}
function cleanup {
if [ ! -z "${GITHUB_ACTION:-}" ]; then
echo "::endgroup::"
fi
}
DSCANNER_DIR="$(dirname -- $( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ))"
if [ ! -z "${GITHUB_ACTION:-}" ]; then
echo "::group::Building d-scanner"
fi
trap cleanup EXIT
trap error ERR
if [ -z "${CI:-}" ]; then
dub build --root="$DSCANNER_DIR"
fi
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)
diff <(../bin/dscanner --resolveMessage b16 it/autofix_ide/source_autofix.d | jq -S .) <(jq -S . it/autofix_ide/source_autofix.autofix.json)
# CLI tests
# ---------
# check that `dscanner fix` works as expected
section '1. test no changes if EOFing'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
section '2. test no changes for simple enter pressing'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "\n" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
section '2.1. test no changes entering 0'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "0\n" | ../bin/dscanner fix it/autofix_cli/test.d
diff it/autofix_cli/test.d it/autofix_cli/source.d
section '3. test change applies automatically with --applySingle'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
../bin/dscanner fix --applySingle it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
section '4. test change apply when entering "1"'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
section '5. test invalid selection reasks what to apply'
cp -v it/autofix_cli/source.d it/autofix_cli/test.d
printf "2\n-1\n1000\na\n1\n" | ../bin/dscanner fix it/autofix_cli/test.d | grep -F 'Writing changes to it/autofix_cli/test.d'
diff it/autofix_cli/test.d it/autofix_cli/fixed.d
# check that `dscanner @myargs.rst` reads arguments from file
section "Test @myargs.rst"
echo "-f" > "myargs.rst"
echo "github" >> "myargs.rst"
echo "lint" >> "myargs.rst"
echo "it/singleissue.d" >> "myargs.rst"
diff it/singleissue_github.txt <(../bin/dscanner "@myargs.rst")
rm "myargs.rst"

1
tests/it/autofix_cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
test.d

View File

@ -0,0 +1,3 @@
void main()
{
}

View File

@ -0,0 +1,3 @@
auto main()
{
}

View File

@ -0,0 +1,38 @@
[
{
"name": "Mark function `const`",
"replacements": [
{
"newText": " const",
"range": [
24,
24
]
}
]
},
{
"name": "Mark function `inout`",
"replacements": [
{
"newText": " inout",
"range": [
24,
24
]
}
]
},
{
"name": "Mark function `immutable`",
"replacements": [
{
"newText": " immutable",
"range": [
24,
24
]
}
]
}
]

View File

@ -0,0 +1,12 @@
struct S
{
int myProp() @property
{
static if (a)
{
}
else if (b)
{
}
}
}

View File

@ -0,0 +1,96 @@
{
"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": "Mark function `const`",
"replacements": [
{
"newText": " const",
"range": [
24,
24
]
}
]
},
{
"name": "Mark function `inout`",
"replacements": [
{
"newText": " inout",
"range": [
24,
24
]
}
]
},
{
"name": "Mark function `immutable`",
"replacements": [
{
"newText": " immutable",
"range": [
24,
24
]
}
]
}
]
},
{
"autofixes": [
{
"name": "Insert `static`",
"replacements": [
{
"newText": "static ",
"range": [
69,
69
]
}
]
},
{
"name": "Wrap '{}' block around 'if'",
"replacements": "resolvable"
}
],
"column": 3,
"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": 3,
"statementCount": 4,
"structCount": 1,
"templateCount": 0,
"undocumentedPublicSymbols": 0
}

1
tests/it/singleissue.d Normal file
View File

@ -0,0 +1 @@
int NonMatchingName;

View File

@ -0,0 +1 @@
::warning file=it/singleissue.d,line=1,endLine=1,col=5,endColumn=20,title=Warning (style_check)::Variable name 'NonMatchingName' does not match style guidelines.