Compare commits

...

36 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
85 changed files with 1742 additions and 543 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

@ -91,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:
@ -101,7 +106,7 @@ dscanner -S -f github source/
dscanner -S -f '{filepath}({line}:{column})[{type}]: {message}' source/
```
To collect automatic issue fixes for a given location use
To resolve automatic issue fixes for a given location use
```sh
# collecting automatic issue fixes
@ -197,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.
@ -301,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.2",
"libdparse": "0.25.0",
"stdx-allocator": "2.77.5"
}
}

@ -1 +1 @@
Subproject commit fe6d1e38fb4fc04323170389cfec67ed7fd4e24a
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)
@ -81,7 +82,8 @@ public:
}
else
addErrorMessage(autoTokens, KEY, MESSAGE,
[AutoFix.replacement(autoTokens[0], "void")]);
[AutoFix.replacement(autoTokens[0], "", "Replace `auto` with `void`")
.concat(AutoFix.insertionAt(decl.name.index, "void "))]);
}
}
@ -195,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;}
@ -203,6 +208,7 @@ unittest
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
AutoFunctionChecker.MESSAGE,
), sac);
assertAnalyzerWarnings(q{
@ -272,13 +278,19 @@ unittest
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,10 +1,12 @@
module dscanner.analysis.base;
import dparse.ast;
import dparse.lexer : IdType, str, Token;
import dparse.lexer : IdType, str, Token, tok;
import dscanner.analysis.nolint;
import dsymbol.scope_ : Scope;
import std.array;
import std.container;
import std.meta : AliasSeq;
import std.string;
import std.sumtype;
@ -369,14 +371,41 @@ 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;
}
@ -404,6 +433,35 @@ 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,
@ -422,6 +480,8 @@ protected:
bool inAggregate;
bool skipTests;
const(Token)[] tokens;
NoLint noLint;
template visitTemplate(T)
{
@ -436,42 +496,58 @@ 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, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, node, message), key, autofixes);
}
void addErrorMessage(const Token token, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, token, message), key, autofixes);
}
void addErrorMessage(const Token[] tokens, string key, string message, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
addErrorMessage(Message.Diagnostic.from(fileName, tokens, message), key, autofixes);
}
void addErrorMessage(size_t[2] index, size_t line, size_t[2] columns, string key, string message, AutoFix[] autofixes = null)
{
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, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
auto d = Message.Diagnostic.from(fileName, index, lines, columns, message);
_messages.insert(Message(d, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, string key, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, key, getName(), autofixes));
}
void addErrorMessage(Message.Diagnostic diagnostic, Message.Diagnostic[] supplemental, string key, AutoFix[] autofixes = null)
{
if (noLint.containsCheck(key))
return;
_messages.insert(Message(diagnostic, supplemental, key, getName(), autofixes));
}
@ -498,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;

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,19 +20,21 @@ 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",
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

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,7 @@ 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)]);
}
@ -149,6 +149,8 @@ final class DuplicateAttributeCheck : BaseAnalyzer
return null;
}
private enum string KEY = "dscanner.unnecessary.duplicate_attribute";
}
unittest

View File

@ -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;
@ -47,7 +47,7 @@ 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.",
@ -58,6 +58,8 @@ final class EnumArrayLiteralCheck : BaseAnalyzer
}
autoDec.accept(this);
}
private enum string KEY = "dscanner.performance.enum_array_literal";
}
unittest

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)

View File

@ -74,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)

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)

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

@ -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,9 +16,9 @@ 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)
@ -49,8 +49,7 @@ final class LambdaReturnCheck : BaseAnalyzer
.concat(AutoFix.insertionAfter(fLit.tokens[0], ")"))
.concat(AutoFix.replacement(arrow[0], ""));
}
autofixes ~= AutoFix.insertionBefore(*endIncl, "(", "Add parenthesis (return delegate)")
.concat(AutoFix.insertionAfter(fe.specifiedFunctionBody.tokens[$ - 1], ")"));
autofixes ~= AutoFix.insertionBefore(*endIncl, "() ", "Add parenthesis (return delegate)");
addErrorMessage(tokens, KEY, "This lambda returns a lambda. Add parenthesis to clarify.",
autofixes);
}
@ -101,11 +100,11 @@ unittest
{
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
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:1
pragma(msg, typeof((a) { return a; })); // fix:0
pragma(msg, typeof((a) => ({ return a; }))); // fix:1
pragma(msg, typeof((a) => () { return a; })); // fix:1
}
}c, sac);

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,7 +42,7 @@ final class LengthSubtractionCheck : BaseAnalyzer
if (l.identifierOrTemplateInstance is null
|| l.identifierOrTemplateInstance.identifier.text != "length")
goto end;
addErrorMessage(addExpression, "dscanner.suspicious.length_subtraction",
addErrorMessage(addExpression, KEY,
"Avoid subtracting from '.length' as it may be unsigned.",
[
AutoFix.insertionBefore(l.tokens[0], "cast(ptrdiff_t) ", "Cast to ptrdiff_t")

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

View File

@ -73,6 +73,7 @@ import dscanner.analysis.final_attribute;
import dscanner.analysis.vcall_in_ctor;
import dscanner.analysis.useless_initializer;
import dscanner.analysis.allman;
import dscanner.analysis.always_curly;
import dscanner.analysis.redundant_attributes;
import dscanner.analysis.has_public_example;
import dscanner.analysis.assert_without_msg;
@ -133,7 +134,8 @@ private string formatContext(Message.Diagnostic diagnostic, scope const(char)[]
import std.string : indexOf, lastIndexOf;
if (diagnostic.startIndex >= diagnostic.endIndex || diagnostic.endIndex > code.length
|| diagnostic.startColumn >= diagnostic.endColumn || diagnostic.endColumn == 0)
|| diagnostic.startColumn >= diagnostic.endColumn || diagnostic.endColumn == 0
|| diagnostic.startColumn == 0)
return null;
auto lineStart = code.lastIndexOf('\n', diagnostic.startIndex) + 1;
@ -305,7 +307,7 @@ void generateReport(string[] fileNames, const StaticAnalysisConfig config,
};
first = true;
StatsCollector stats = new StatsCollector("");
StatsCollector stats = new StatsCollector(BaseAnalyzerArguments.init);
ulong lineOfCodeCount;
foreach (fileName; fileNames)
{
@ -611,12 +613,12 @@ private struct UserSelect
if (special.shorthands.canFind(input))
return special.id;
int item = input.to!int;
if (item < 0 || item > regularItems.length)
int item = input.to!int - 1;
if (item < 0 || item >= regularItems.length)
throw new Exception("Selected option number out of range.");
return item;
}
catch (ConvException e)
catch (Exception e)
{
writeln("Invalid selection, try again. ", e.message);
}
@ -747,216 +749,226 @@ private BaseAnalyzer[] getAnalyzersForModuleAndConfig(string fileName,
m.moduleDeclaration.moduleName.identifiers !is null)
moduleName = m.moduleDeclaration.moduleName.identifiers.map!(e => e.text).join(".");
BaseAnalyzerArguments args = BaseAnalyzerArguments(
fileName,
tokens,
moduleScope
);
if (moduleName.shouldRun!AsmStyleCheck(analysisConfig))
checks ~= new AsmStyleCheck(fileName, moduleScope,
analysisConfig.asm_style_check == Check.skipTests && !ut);
checks ~= new AsmStyleCheck(args.setSkipTests(
analysisConfig.asm_style_check == Check.skipTests && !ut));
if (moduleName.shouldRun!BackwardsRangeCheck(analysisConfig))
checks ~= new BackwardsRangeCheck(fileName, moduleScope,
analysisConfig.backwards_range_check == Check.skipTests && !ut);
checks ~= new BackwardsRangeCheck(args.setSkipTests(
analysisConfig.backwards_range_check == Check.skipTests && !ut));
if (moduleName.shouldRun!BuiltinPropertyNameCheck(analysisConfig))
checks ~= new BuiltinPropertyNameCheck(fileName, moduleScope,
analysisConfig.builtin_property_names_check == Check.skipTests && !ut);
checks ~= new BuiltinPropertyNameCheck(args.setSkipTests(
analysisConfig.builtin_property_names_check == Check.skipTests && !ut));
if (moduleName.shouldRun!CommaExpressionCheck(analysisConfig))
checks ~= new CommaExpressionCheck(fileName, moduleScope,
analysisConfig.comma_expression_check == Check.skipTests && !ut);
checks ~= new CommaExpressionCheck(args.setSkipTests(
analysisConfig.comma_expression_check == Check.skipTests && !ut));
if (moduleName.shouldRun!ConstructorCheck(analysisConfig))
checks ~= new ConstructorCheck(fileName, moduleScope,
analysisConfig.constructor_check == Check.skipTests && !ut);
checks ~= new ConstructorCheck(args.setSkipTests(
analysisConfig.constructor_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UnmodifiedFinder(analysisConfig))
checks ~= new UnmodifiedFinder(fileName, moduleScope,
analysisConfig.could_be_immutable_check == Check.skipTests && !ut);
checks ~= new UnmodifiedFinder(args.setSkipTests(
analysisConfig.could_be_immutable_check == Check.skipTests && !ut));
if (moduleName.shouldRun!DeleteCheck(analysisConfig))
checks ~= new DeleteCheck(fileName, moduleScope,
analysisConfig.delete_check == Check.skipTests && !ut);
checks ~= new DeleteCheck(args.setSkipTests(
analysisConfig.delete_check == Check.skipTests && !ut));
if (moduleName.shouldRun!DuplicateAttributeCheck(analysisConfig))
checks ~= new DuplicateAttributeCheck(fileName, moduleScope,
analysisConfig.duplicate_attribute == Check.skipTests && !ut);
checks ~= new DuplicateAttributeCheck(args.setSkipTests(
analysisConfig.duplicate_attribute == Check.skipTests && !ut));
if (moduleName.shouldRun!EnumArrayLiteralCheck(analysisConfig))
checks ~= new EnumArrayLiteralCheck(fileName, moduleScope,
analysisConfig.enum_array_literal_check == Check.skipTests && !ut);
checks ~= new EnumArrayLiteralCheck(args.setSkipTests(
analysisConfig.enum_array_literal_check == Check.skipTests && !ut));
if (moduleName.shouldRun!PokemonExceptionCheck(analysisConfig))
checks ~= new PokemonExceptionCheck(fileName, moduleScope,
analysisConfig.exception_check == Check.skipTests && !ut);
checks ~= new PokemonExceptionCheck(args.setSkipTests(
analysisConfig.exception_check == Check.skipTests && !ut));
if (moduleName.shouldRun!FloatOperatorCheck(analysisConfig))
checks ~= new FloatOperatorCheck(fileName, moduleScope,
analysisConfig.float_operator_check == Check.skipTests && !ut);
checks ~= new FloatOperatorCheck(args.setSkipTests(
analysisConfig.float_operator_check == Check.skipTests && !ut));
if (moduleName.shouldRun!FunctionAttributeCheck(analysisConfig))
checks ~= new FunctionAttributeCheck(fileName, moduleScope,
analysisConfig.function_attribute_check == Check.skipTests && !ut);
checks ~= new FunctionAttributeCheck(args.setSkipTests(
analysisConfig.function_attribute_check == Check.skipTests && !ut));
if (moduleName.shouldRun!IfElseSameCheck(analysisConfig))
checks ~= new IfElseSameCheck(fileName, moduleScope,
analysisConfig.if_else_same_check == Check.skipTests&& !ut);
checks ~= new IfElseSameCheck(args.setSkipTests(
analysisConfig.if_else_same_check == Check.skipTests&& !ut));
if (moduleName.shouldRun!LabelVarNameCheck(analysisConfig))
checks ~= new LabelVarNameCheck(fileName, moduleScope,
analysisConfig.label_var_same_name_check == Check.skipTests && !ut);
checks ~= new LabelVarNameCheck(args.setSkipTests(
analysisConfig.label_var_same_name_check == Check.skipTests && !ut));
if (moduleName.shouldRun!LengthSubtractionCheck(analysisConfig))
checks ~= new LengthSubtractionCheck(fileName, moduleScope,
analysisConfig.length_subtraction_check == Check.skipTests && !ut);
checks ~= new LengthSubtractionCheck(args.setSkipTests(
analysisConfig.length_subtraction_check == Check.skipTests && !ut));
if (moduleName.shouldRun!LocalImportCheck(analysisConfig))
checks ~= new LocalImportCheck(fileName, moduleScope,
analysisConfig.local_import_check == Check.skipTests && !ut);
checks ~= new LocalImportCheck(args.setSkipTests(
analysisConfig.local_import_check == Check.skipTests && !ut));
if (moduleName.shouldRun!LogicPrecedenceCheck(analysisConfig))
checks ~= new LogicPrecedenceCheck(fileName, moduleScope,
analysisConfig.logical_precedence_check == Check.skipTests && !ut);
checks ~= new LogicPrecedenceCheck(args.setSkipTests(
analysisConfig.logical_precedence_check == Check.skipTests && !ut));
if (moduleName.shouldRun!MismatchedArgumentCheck(analysisConfig))
checks ~= new MismatchedArgumentCheck(fileName, moduleScope,
analysisConfig.mismatched_args_check == Check.skipTests && !ut);
checks ~= new MismatchedArgumentCheck(args.setSkipTests(
analysisConfig.mismatched_args_check == Check.skipTests && !ut));
if (moduleName.shouldRun!NumberStyleCheck(analysisConfig))
checks ~= new NumberStyleCheck(fileName, moduleScope,
analysisConfig.number_style_check == Check.skipTests && !ut);
checks ~= new NumberStyleCheck(args.setSkipTests(
analysisConfig.number_style_check == Check.skipTests && !ut));
if (moduleName.shouldRun!ObjectConstCheck(analysisConfig))
checks ~= new ObjectConstCheck(fileName, moduleScope,
analysisConfig.object_const_check == Check.skipTests && !ut);
checks ~= new ObjectConstCheck(args.setSkipTests(
analysisConfig.object_const_check == Check.skipTests && !ut));
if (moduleName.shouldRun!OpEqualsWithoutToHashCheck(analysisConfig))
checks ~= new OpEqualsWithoutToHashCheck(fileName, moduleScope,
analysisConfig.opequals_tohash_check == Check.skipTests && !ut);
checks ~= new OpEqualsWithoutToHashCheck(args.setSkipTests(
analysisConfig.opequals_tohash_check == Check.skipTests && !ut));
if (moduleName.shouldRun!RedundantParenCheck(analysisConfig))
checks ~= new RedundantParenCheck(fileName, moduleScope,
analysisConfig.redundant_parens_check == Check.skipTests && !ut);
checks ~= new RedundantParenCheck(args.setSkipTests(
analysisConfig.redundant_parens_check == Check.skipTests && !ut));
if (moduleName.shouldRun!StyleChecker(analysisConfig))
checks ~= new StyleChecker(fileName, moduleScope,
analysisConfig.style_check == Check.skipTests && !ut);
checks ~= new StyleChecker(args.setSkipTests(
analysisConfig.style_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UndocumentedDeclarationCheck(analysisConfig))
checks ~= new UndocumentedDeclarationCheck(fileName, moduleScope,
analysisConfig.undocumented_declaration_check == Check.skipTests && !ut);
checks ~= new UndocumentedDeclarationCheck(args.setSkipTests(
analysisConfig.undocumented_declaration_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UnusedLabelCheck(analysisConfig))
checks ~= new UnusedLabelCheck(fileName, moduleScope,
analysisConfig.unused_label_check == Check.skipTests && !ut);
checks ~= new UnusedLabelCheck(args.setSkipTests(
analysisConfig.unused_label_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UnusedVariableCheck(analysisConfig))
checks ~= new UnusedVariableCheck(fileName, moduleScope,
analysisConfig.unused_variable_check == Check.skipTests && !ut);
checks ~= new UnusedVariableCheck(args.setSkipTests(
analysisConfig.unused_variable_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UnusedParameterCheck(analysisConfig))
checks ~= new UnusedParameterCheck(fileName, moduleScope,
analysisConfig.unused_parameter_check == Check.skipTests && !ut);
checks ~= new UnusedParameterCheck(args.setSkipTests(
analysisConfig.unused_parameter_check == Check.skipTests && !ut));
if (moduleName.shouldRun!LineLengthCheck(analysisConfig))
checks ~= new LineLengthCheck(fileName, tokens,
analysisConfig.max_line_length,
analysisConfig.long_line_check == Check.skipTests && !ut);
checks ~= new LineLengthCheck(args.setSkipTests(
analysisConfig.long_line_check == Check.skipTests && !ut),
analysisConfig.max_line_length);
if (moduleName.shouldRun!AutoRefAssignmentCheck(analysisConfig))
checks ~= new AutoRefAssignmentCheck(fileName,
analysisConfig.auto_ref_assignment_check == Check.skipTests && !ut);
checks ~= new AutoRefAssignmentCheck(args.setSkipTests(
analysisConfig.auto_ref_assignment_check == Check.skipTests && !ut));
if (moduleName.shouldRun!IncorrectInfiniteRangeCheck(analysisConfig))
checks ~= new IncorrectInfiniteRangeCheck(fileName,
analysisConfig.incorrect_infinite_range_check == Check.skipTests && !ut);
checks ~= new IncorrectInfiniteRangeCheck(args.setSkipTests(
analysisConfig.incorrect_infinite_range_check == Check.skipTests && !ut));
if (moduleName.shouldRun!UselessAssertCheck(analysisConfig))
checks ~= new UselessAssertCheck(fileName,
analysisConfig.useless_assert_check == Check.skipTests && !ut);
checks ~= new UselessAssertCheck(args.setSkipTests(
analysisConfig.useless_assert_check == Check.skipTests && !ut));
if (moduleName.shouldRun!AliasSyntaxCheck(analysisConfig))
checks ~= new AliasSyntaxCheck(fileName,
analysisConfig.alias_syntax_check == Check.skipTests && !ut);
checks ~= new AliasSyntaxCheck(args.setSkipTests(
analysisConfig.alias_syntax_check == Check.skipTests && !ut));
if (moduleName.shouldRun!StaticIfElse(analysisConfig))
checks ~= new StaticIfElse(fileName,
analysisConfig.static_if_else_check == Check.skipTests && !ut);
checks ~= new StaticIfElse(args.setSkipTests(
analysisConfig.static_if_else_check == Check.skipTests && !ut));
if (moduleName.shouldRun!LambdaReturnCheck(analysisConfig))
checks ~= new LambdaReturnCheck(fileName,
analysisConfig.lambda_return_check == Check.skipTests && !ut);
checks ~= new LambdaReturnCheck(args.setSkipTests(
analysisConfig.lambda_return_check == Check.skipTests && !ut));
if (moduleName.shouldRun!AutoFunctionChecker(analysisConfig))
checks ~= new AutoFunctionChecker(fileName,
analysisConfig.auto_function_check == Check.skipTests && !ut);
checks ~= new AutoFunctionChecker(args.setSkipTests(
analysisConfig.auto_function_check == Check.skipTests && !ut));
if (moduleName.shouldRun!ImportSortednessCheck(analysisConfig))
checks ~= new ImportSortednessCheck(fileName,
analysisConfig.imports_sortedness == Check.skipTests && !ut);
checks ~= new ImportSortednessCheck(args.setSkipTests(
analysisConfig.imports_sortedness == Check.skipTests && !ut));
if (moduleName.shouldRun!ExplicitlyAnnotatedUnittestCheck(analysisConfig))
checks ~= new ExplicitlyAnnotatedUnittestCheck(fileName,
analysisConfig.explicitly_annotated_unittests == Check.skipTests && !ut);
checks ~= new ExplicitlyAnnotatedUnittestCheck(args.setSkipTests(
analysisConfig.explicitly_annotated_unittests == Check.skipTests && !ut));
if (moduleName.shouldRun!ProperlyDocumentedPublicFunctions(analysisConfig))
checks ~= new ProperlyDocumentedPublicFunctions(fileName,
analysisConfig.properly_documented_public_functions == Check.skipTests && !ut);
checks ~= new ProperlyDocumentedPublicFunctions(args.setSkipTests(
analysisConfig.properly_documented_public_functions == Check.skipTests && !ut));
if (moduleName.shouldRun!FinalAttributeChecker(analysisConfig))
checks ~= new FinalAttributeChecker(fileName,
analysisConfig.final_attribute_check == Check.skipTests && !ut);
checks ~= new FinalAttributeChecker(args.setSkipTests(
analysisConfig.final_attribute_check == Check.skipTests && !ut));
if (moduleName.shouldRun!VcallCtorChecker(analysisConfig))
checks ~= new VcallCtorChecker(fileName,
analysisConfig.vcall_in_ctor == Check.skipTests && !ut);
checks ~= new VcallCtorChecker(args.setSkipTests(
analysisConfig.vcall_in_ctor == Check.skipTests && !ut));
if (moduleName.shouldRun!UselessInitializerChecker(analysisConfig))
checks ~= new UselessInitializerChecker(fileName,
analysisConfig.useless_initializer == Check.skipTests && !ut);
checks ~= new UselessInitializerChecker(args.setSkipTests(
analysisConfig.useless_initializer == Check.skipTests && !ut));
if (moduleName.shouldRun!AllManCheck(analysisConfig))
checks ~= new AllManCheck(fileName, tokens,
analysisConfig.allman_braces_check == Check.skipTests && !ut);
checks ~= new AllManCheck(args.setSkipTests(
analysisConfig.allman_braces_check == Check.skipTests && !ut));
if (moduleName.shouldRun!AlwaysCurlyCheck(analysisConfig))
checks ~= new AlwaysCurlyCheck(args.setSkipTests(
analysisConfig.always_curly_check == Check.skipTests && !ut));
if (moduleName.shouldRun!RedundantAttributesCheck(analysisConfig))
checks ~= new RedundantAttributesCheck(fileName, moduleScope,
analysisConfig.redundant_attributes_check == Check.skipTests && !ut);
checks ~= new RedundantAttributesCheck(args.setSkipTests(
analysisConfig.redundant_attributes_check == Check.skipTests && !ut));
if (moduleName.shouldRun!HasPublicExampleCheck(analysisConfig))
checks ~= new HasPublicExampleCheck(fileName, moduleScope,
analysisConfig.has_public_example == Check.skipTests && !ut);
checks ~= new HasPublicExampleCheck(args.setSkipTests(
analysisConfig.has_public_example == Check.skipTests && !ut));
if (moduleName.shouldRun!AssertWithoutMessageCheck(analysisConfig))
checks ~= new AssertWithoutMessageCheck(fileName, moduleScope,
analysisConfig.assert_without_msg == Check.skipTests && !ut);
checks ~= new AssertWithoutMessageCheck(args.setSkipTests(
analysisConfig.assert_without_msg == Check.skipTests && !ut));
if (moduleName.shouldRun!IfConstraintsIndentCheck(analysisConfig))
checks ~= new IfConstraintsIndentCheck(fileName, tokens,
analysisConfig.if_constraints_indent == Check.skipTests && !ut);
checks ~= new IfConstraintsIndentCheck(args.setSkipTests(
analysisConfig.if_constraints_indent == Check.skipTests && !ut));
if (moduleName.shouldRun!TrustTooMuchCheck(analysisConfig))
checks ~= new TrustTooMuchCheck(fileName,
analysisConfig.trust_too_much == Check.skipTests && !ut);
checks ~= new TrustTooMuchCheck(args.setSkipTests(
analysisConfig.trust_too_much == Check.skipTests && !ut));
if (moduleName.shouldRun!RedundantStorageClassCheck(analysisConfig))
checks ~= new RedundantStorageClassCheck(fileName,
analysisConfig.redundant_storage_classes == Check.skipTests && !ut);
checks ~= new RedundantStorageClassCheck(args.setSkipTests(
analysisConfig.redundant_storage_classes == Check.skipTests && !ut));
if (moduleName.shouldRun!UnusedResultChecker(analysisConfig))
checks ~= new UnusedResultChecker(fileName, moduleScope,
analysisConfig.unused_result == Check.skipTests && !ut);
checks ~= new UnusedResultChecker(args.setSkipTests(
analysisConfig.unused_result == Check.skipTests && !ut));
if (moduleName.shouldRun!CyclomaticComplexityCheck(analysisConfig))
checks ~= new CyclomaticComplexityCheck(fileName, moduleScope,
analysisConfig.cyclomatic_complexity == Check.skipTests && !ut,
checks ~= new CyclomaticComplexityCheck(args.setSkipTests(
analysisConfig.cyclomatic_complexity == Check.skipTests && !ut),
analysisConfig.max_cyclomatic_complexity.to!int);
if (moduleName.shouldRun!BodyOnDisabledFuncsCheck(analysisConfig))
checks ~= new BodyOnDisabledFuncsCheck(fileName, moduleScope,
analysisConfig.body_on_disabled_func_check == Check.skipTests && !ut);
checks ~= new BodyOnDisabledFuncsCheck(args.setSkipTests(
analysisConfig.body_on_disabled_func_check == Check.skipTests && !ut));
version (none)
if (moduleName.shouldRun!IfStatementCheck(analysisConfig))
checks ~= new IfStatementCheck(fileName, moduleScope,
analysisConfig.redundant_if_check == Check.skipTests && !ut);
checks ~= new IfStatementCheck(args.setSkipTests(
analysisConfig.redundant_if_check == Check.skipTests && !ut));
return checks;
}

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)

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;
@ -64,23 +65,29 @@ else
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,
@ -102,6 +109,7 @@ else
"resolveMessage", &resolveMessage,
"applySingle", &applySingleFixes,
"I", &importPaths,
"exclude", &excludePaths,
"version", &printVersion,
"muffinButton", &muffin,
"explore", &explore,
@ -191,6 +199,24 @@ else
}
}
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)
@ -202,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;
@ -253,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)
@ -278,15 +303,15 @@ 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 || autofix || resolveMessage.length)
{
@ -299,7 +324,7 @@ else
if (autofix)
{
return .autofix(expandArgs(args), config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0;
return .autofix(expandedArgs, config, errorFormat, cache, moduleCache, applySingleFixes) ? 1 : 0;
}
else if (resolveMessage.length)
{
@ -315,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
{
@ -346,7 +371,7 @@ else
else
{
ulong count;
foreach (f; expandArgs(args))
foreach (f; expandedArgs)
{
LexerConfig config;
@ -393,7 +418,7 @@ else
void printHelp(string programName)
{
stderr.writefln(`
stdout.writefln(`
Usage: %1$s <options>
Human-readable output:
@ -444,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,
@ -546,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)
{
@ -128,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:

View File

@ -1,7 +1,7 @@
; 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="disabled"
style_check="enabled"
; Check for array literals that cause unnecessary allocation
enum_array_literal_check="enabled"
; Check for poor exception handling practices

View File

@ -2,15 +2,81 @@
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 ))"
dub build --root="$DSCANNER_DIR"
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 <(jq -S . <(../bin/dscanner --report it/source_autofix.d)) <(jq -S . it/source_autofix.report.json)
diff <(jq -S . <(../bin/dscanner --resolveMessage b16 it/source_autofix.d)) <(jq -S . it/source_autofix.autofix.json)
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,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.

View File

@ -1,6 +0,0 @@
struct S
{
int myProp() @property
{
}
}

View File

@ -1,26 +0,0 @@
{
"classCount": 0,
"functionCount": 1,
"interfaceCount": 0,
"issues": [
{
"column": 6,
"endColumn": 12,
"endIndex": 22,
"endLine": 3,
"fileName": "it\/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"
}
],
"lineOfCodeCount": 0,
"statementCount": 0,
"structCount": 1,
"templateCount": 0,
"undocumentedPublicSymbols": 0
}