├── .gitignore ├── LICENSE.txt ├── README.md ├── dub.sdl ├── dub.selections.json ├── source └── dlint │ ├── app.d │ └── checks │ └── undocumented.d └── test ├── Makefile └── undocumented ├── t01_basic.d ├── t01_basic.d.out ├── t02_special.d ├── t02_special.d.out ├── t03_convention.d ├── t03_convention.d.out ├── t04_deprecated.d ├── t04_deprecated.d.out ├── t05_disabled.d ├── t05_disabled.d.out ├── t06_eponymous.d └── t06_eponymous.d.out /.gitignore: -------------------------------------------------------------------------------- 1 | /dlint 2 | /.dub/ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dlint 2 | ===== 3 | 4 | *dlint* is a linter for D code. 5 | 6 | Unlike the leading D linting project, [DScanner](https://github.com/dlang-community/D-Scanner), *dlint* uses the DMD compiler front-end to parse D code. It is therefore able to perform some checks which are infeasible to accurately implement in DScanner. 7 | 8 | **WORK IN PROGRESS** 9 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "dlint" 2 | description "DMDFE-based D linter." 3 | authors "Vladimir Panteleev" 4 | copyright "Copyright © 2021, Vladimir Panteleev" 5 | license "BSL-1.0" 6 | dependency "dmd" version="~master" 7 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dmd": "~master" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /source/dlint/app.d: -------------------------------------------------------------------------------- 1 | module dlint.app; 2 | 3 | import core.stdc.stdio; 4 | 5 | import std.algorithm.searching : startsWith; 6 | import std.typecons : Tuple; 7 | 8 | import dmd.astcodegen; 9 | import dmd.dmodule : Module; 10 | import dmd.frontend; 11 | import dmd.globals; 12 | 13 | import dlint.checks.undocumented; 14 | 15 | Tuple!(Module, "module_", Diagnostics, "diagnostics") parseModule(AST = ASTCodegen)( 16 | const(char)[] fileName) 17 | { 18 | import dmd.root.file : File, FileBuffer; 19 | 20 | import dmd.globals : Loc, global; 21 | import dmd.parse : Parser; 22 | import dmd.identifier : Identifier; 23 | import dmd.tokens : TOK; 24 | 25 | import std.path : baseName, stripExtension; 26 | import std.string : toStringz; 27 | import std.typecons : tuple; 28 | 29 | auto id = Identifier.idPool(fileName.baseName.stripExtension); 30 | auto m = new Module(fileName, id, 1, 0); 31 | 32 | m.read(Loc.initial); 33 | 34 | m.parseModule!AST(); 35 | 36 | Diagnostics diagnostics = { 37 | errors: global.errors, 38 | warnings: global.warnings 39 | }; 40 | 41 | return typeof(return)(m, diagnostics); 42 | } 43 | 44 | void main(string[] args) 45 | { 46 | initDMD; 47 | global.params.showColumns = true; 48 | 49 | import std.algorithm : each; 50 | findImportPaths.each!addImport; 51 | 52 | import std.file; 53 | 54 | Module[] modules; 55 | 56 | foreach (arg; args[1..$]) 57 | { 58 | if (arg.startsWith("-")) 59 | { 60 | switch (arg) 61 | { 62 | case "-unittest": 63 | case "-d": 64 | case "-dw": 65 | case "-de": 66 | break; // ignore 67 | default: 68 | if (arg.startsWith("-I")) 69 | addImport(arg[2 .. $]); 70 | else 71 | throw new Exception("Unknown switch: " ~ arg); 72 | } 73 | continue; 74 | } 75 | 76 | debug(dlint) printf("# Loading %.*s\n", 77 | cast(int)arg.length, arg.ptr); 78 | 79 | auto t = parseModule(arg); 80 | 81 | assert(!t.diagnostics.hasErrors); 82 | assert(!t.diagnostics.hasWarnings); 83 | 84 | modules ~= t.module_; 85 | } 86 | 87 | auto linter = new UndocumentedLinter; 88 | 89 | foreach (m; modules) 90 | { 91 | debug(dlint) printf("# Processing %s\n", 92 | m.srcfile.toChars()); 93 | 94 | m.fullSemantic; 95 | m.accept(linter); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /source/dlint/checks/undocumented.d: -------------------------------------------------------------------------------- 1 | module dlint.checks.undocumented; 2 | 3 | import core.internal.traits : Parameters; 4 | import core.stdc.stdio; 5 | 6 | import std.algorithm.searching : startsWith; 7 | 8 | import dmd.astcodegen; 9 | import dmd.visitor; 10 | 11 | extern(C++) final class UndocumentedLinter : SemanticTimeTransitiveVisitor 12 | { 13 | alias visit = typeof(super).visit; 14 | 15 | alias AST = ASTCodegen; 16 | debug(dlint) int depth; 17 | AST.Visibility.Kind currentVisibility = AST.Visibility.Kind.public_; 18 | bool inEponymous; 19 | 20 | // We do this because the TransitiveVisitor does not forward 21 | // visit(FooDeclaration) to visit(Declaration) 22 | static foreach (overload; __traits(getOverloads, Visitor, "visit")) 23 | override void visit(Parameters!overload[0] d) 24 | { 25 | debug(dlint) { depth++; scope(success) depth--; } 26 | 27 | void log()(string s) 28 | { 29 | const(char)* loc; 30 | static if (is(typeof(d.loc))) 31 | loc = d.loc.toChars(); 32 | else 33 | loc = "-"; 34 | debug(dlint) printf("%*s# %s: %.*s %s %s\n", 35 | depth, "".ptr, 36 | loc, 37 | cast(int)s.length, s.ptr, 38 | typeof(d).stringof.ptr, 39 | d.toChars()); 40 | } 41 | 42 | bool ignoreCurrent; 43 | if (inEponymous) 44 | { 45 | inEponymous = false; 46 | ignoreCurrent = true; 47 | } 48 | 49 | if (isDeprecated(d)) 50 | return log("Skipping deprecated"); 51 | 52 | static if (is(typeof(d) == AST.TemplateDeclaration)) 53 | if (d.onemember) 54 | { 55 | /// DMD moves the "deprecated" attribute on the 56 | /// inner symbol for eponymous templates. 57 | if (isDeprecated(d.onemember)) 58 | return log("Skipping deprecated eponymous"); 59 | 60 | log("Diving inside eponymous"); 61 | if (!ignoreCurrent) 62 | if (!checkThing(d)) // outer 63 | return; 64 | inEponymous = true; 65 | d.onemember.accept(this); 66 | return; 67 | } 68 | 69 | static if (is(typeof(d) == AST.Import) 70 | || is(typeof(d) == AST.CompoundStatement) 71 | || is(typeof(d) == AST.FuncLiteralDeclaration) 72 | || is(typeof(d) == AST.ThisDeclaration) 73 | || is(typeof(d) == AST.DeprecatedDeclaration)) 74 | { 75 | // Does not need to be documented or traversed 76 | debug(dlint) log("Skipping"); 77 | } 78 | else 79 | // We do this because e.g. Declaration and 80 | // AggregateDeclaration are unrelated types which both 81 | // have a `visibility` field (and their common ancestor 82 | // does not have a `visibility` field). 83 | static if (is(typeof(d.visibility) : AST.Visibility)) 84 | { 85 | auto visibility = currentVisibility; 86 | 87 | static if (is(typeof(d) == AST.VisibilityDeclaration)) 88 | { 89 | // Has visibility, but cannot be documented; 90 | // may contain public members 91 | debug(dlint) printf("%*s# %s: Silently descending into %s %s %d\n", 92 | depth, "".ptr, 93 | d.loc.toChars(), 94 | typeof(d).stringof.ptr, 95 | d.toChars(), 96 | d.visibility.kind); 97 | visibility = d.visibility.kind; 98 | } 99 | else 100 | { 101 | // Should be documented, and traversed 102 | debug(dlint) printf("%*s# %s: %s %s %d\n", 103 | depth, "".ptr, 104 | d.loc.toChars(), 105 | typeof(d).stringof.ptr, 106 | d.toChars(), 107 | d.visibility.kind); 108 | 109 | if (!ignoreCurrent) 110 | if (!checkThing(d)) 111 | return; 112 | } 113 | 114 | static if (is(typeof(d) == AST.AliasDeclaration)) 115 | { 116 | // Should be documented, but must not be traversed 117 | debug(dlint) printf("%*s#(not traversing!)\n", 118 | depth, "".ptr, 119 | d.loc.toChars(), 120 | typeof(d).stringof.ptr, 121 | d.toChars()); 122 | } 123 | else 124 | { 125 | auto lastVisibility = currentVisibility; 126 | currentVisibility = visibility; 127 | scope(success) currentVisibility = lastVisibility; 128 | 129 | super.visit(d); 130 | } 131 | } 132 | else 133 | { 134 | log("Visiting unknown"); 135 | super.visit(d); 136 | } 137 | } 138 | 139 | bool isDeprecated(T)(T d) 140 | { 141 | static if (is(typeof(d.storage_class))) 142 | if (d.storage_class & AST.STC.deprecated_) 143 | return true; 144 | static if (is(typeof(d.stc))) 145 | if (d.stc & AST.STC.deprecated_) 146 | return true; 147 | static if (is(typeof(d) : AST.Dsymbol)) 148 | if (d.isDeprecated()) 149 | return true; 150 | return false; 151 | } 152 | 153 | bool checkThing(T)(T d) 154 | { 155 | if (currentVisibility < AST.Visibility.Kind.public_) 156 | return false; 157 | 158 | // Some declarations need to be public even though 159 | // they should not be (e.g. if they are exposed 160 | // through public aliases). Apply the same 161 | // convention as seen in many other languages 162 | // without visibility as a language feature, and 163 | // treat variable starting with "_" as private. 164 | // (This will also include compiler-generated 165 | // symbols, such as __xpostblit). 166 | static if (!is(typeof(d) == AST.CtorDeclaration)) 167 | if (d.ident && d.ident.toString().startsWith("_")) 168 | return false; 169 | 170 | // Skip compiler-generated declarations 171 | static if (is(typeof(d.generated) : bool)) 172 | if (d.generated) 173 | return false; 174 | if (!d.loc.isValid()) 175 | return false; 176 | 177 | checkDsymbol(typeof(d).stringof.ptr, d); 178 | return true; 179 | } 180 | 181 | void checkDsymbol(const(char)* type, AST.Dsymbol d) 182 | { 183 | if (d.comment) 184 | return; 185 | 186 | printf("%s: Undocumented public declaration: %s `%s`\n", 187 | d.loc.toChars(), 188 | type, d.toChars()); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | DLINT=../dlint 2 | 3 | SOURCES=$(shell find ../source -name '*.d') 4 | TESTS=$(shell find . -name '*.d') 5 | TESTS_OUT=$(addsuffix .out, $(TESTS)) 6 | 7 | all : $(TESTS_OUT) 8 | 9 | $(DLINT) : $(SOURCES) 10 | env -C .. dub 11 | 12 | %.out : % $(DLINT) 13 | $(DLINT) $< &> $@ 14 | 15 | .PHONY: all 16 | .SUFFIXES: 17 | .DELETE_ON_ERROR: 18 | -------------------------------------------------------------------------------- /test/undocumented/t01_basic.d: -------------------------------------------------------------------------------- 1 | void bad01() {} 2 | 3 | private void good01() {} 4 | 5 | private 6 | { 7 | void good02() {} 8 | } 9 | 10 | /// documented! 11 | void good03() {} 12 | 13 | void good04() {} /// ditto 14 | 15 | struct bad1 16 | { 17 | void bad11() {} 18 | private: 19 | void good12() {} 20 | } 21 | 22 | private struct good2 23 | { 24 | void good21() {} 25 | public: 26 | void good22() {} 27 | } 28 | 29 | void bad3() 30 | { 31 | int good31; 32 | } 33 | 34 | struct bad4(T) 35 | { 36 | void bad41() {} 37 | private: 38 | void good42() {} 39 | } 40 | 41 | struct bad5 42 | { 43 | void bad51() {} 44 | private: 45 | void good52() {} 46 | public: 47 | void bad52() {} 48 | } 49 | 50 | class bad6 51 | { 52 | this(int) {} 53 | } 54 | -------------------------------------------------------------------------------- /test/undocumented/t01_basic.d.out: -------------------------------------------------------------------------------- 1 | undocumented/t01_basic.d(1,6): Undocumented public declaration: FuncDeclaration `bad01` 2 | undocumented/t01_basic.d(15,1): Undocumented public declaration: StructDeclaration `bad1` 3 | undocumented/t01_basic.d(17,7): Undocumented public declaration: FuncDeclaration `bad11` 4 | undocumented/t01_basic.d(29,6): Undocumented public declaration: FuncDeclaration `bad3` 5 | undocumented/t01_basic.d(34,1): Undocumented public declaration: TemplateDeclaration `bad4(T)` 6 | undocumented/t01_basic.d(36,7): Undocumented public declaration: FuncDeclaration `bad41` 7 | undocumented/t01_basic.d(41,1): Undocumented public declaration: StructDeclaration `bad5` 8 | undocumented/t01_basic.d(43,7): Undocumented public declaration: FuncDeclaration `bad51` 9 | undocumented/t01_basic.d(47,7): Undocumented public declaration: FuncDeclaration `bad52` 10 | undocumented/t01_basic.d(50,1): Undocumented public declaration: ClassDeclaration `bad6` 11 | undocumented/t01_basic.d(52,2): Undocumented public declaration: CtorDeclaration `this` 12 | -------------------------------------------------------------------------------- /test/undocumented/t02_special.d: -------------------------------------------------------------------------------- 1 | // Special language constructs which should not trigger a positive. 2 | 3 | // --- public imports 4 | public import object; 5 | 6 | // --- compiler-generated methods 7 | private struct WithCopyCtor { this(this) { } } 8 | struct bad01 9 | { 10 | WithCopyCtor c; 11 | } 12 | 13 | // --- lambdas in function declarations 14 | 15 | /// documented! 16 | void fun(alias pred=(a, b) => a is b)() {} 17 | 18 | // --- aliases for overloads 19 | 20 | /// documented! 21 | class Good1 22 | { 23 | /// documented! 24 | void good1() {} 25 | } 26 | 27 | /// also documented! 28 | class Good2 : Good1 29 | { 30 | /// also documented! 31 | alias good1 = Good1.good1; 32 | 33 | /// documented! 34 | void good1(int) {} 35 | } 36 | 37 | // --- nested classes 38 | 39 | /// documented! 40 | class Good3 41 | { 42 | /// also documented! 43 | class Good4 44 | { 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/undocumented/t02_special.d.out: -------------------------------------------------------------------------------- 1 | undocumented/t02_special.d(8,1): Undocumented public declaration: StructDeclaration `bad01` 2 | undocumented/t02_special.d(10,15): Undocumented public declaration: VarDeclaration `c` 3 | -------------------------------------------------------------------------------- /test/undocumented/t03_convention.d: -------------------------------------------------------------------------------- 1 | // Ostensibly private by naming convention 2 | 3 | void bad1() {} 4 | void _good2() {} 5 | void __good3() {} 6 | -------------------------------------------------------------------------------- /test/undocumented/t03_convention.d.out: -------------------------------------------------------------------------------- 1 | undocumented/t03_convention.d(3,6): Undocumented public declaration: FuncDeclaration `bad1` 2 | -------------------------------------------------------------------------------- /test/undocumented/t04_deprecated.d: -------------------------------------------------------------------------------- 1 | // Deprecated symbols are exempt from being documented. 2 | 3 | void bad1() {} 4 | deprecated void good2() {} 5 | deprecated struct good3 6 | { 7 | void good4() {} 8 | } 9 | 10 | deprecated("foo") 11 | template good5() 12 | { 13 | int good5() {} 14 | } 15 | 16 | deprecated 17 | template good6() 18 | { 19 | int good6() {} 20 | } 21 | 22 | deprecated void good7()() 23 | { 24 | } 25 | 26 | static if (false) 27 | deprecated struct Good8 28 | { 29 | int good81; 30 | } 31 | -------------------------------------------------------------------------------- /test/undocumented/t04_deprecated.d.out: -------------------------------------------------------------------------------- 1 | undocumented/t04_deprecated.d(3,6): Undocumented public declaration: FuncDeclaration `bad1` 2 | -------------------------------------------------------------------------------- /test/undocumented/t05_disabled.d: -------------------------------------------------------------------------------- 1 | // Disabled declarations 2 | 3 | version(none): 4 | private: 5 | 6 | class C 7 | { 8 | int i; 9 | } 10 | -------------------------------------------------------------------------------- /test/undocumented/t05_disabled.d.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyberShadow/dlint/f2e548aefc35b33a551d987194e110ad33a4b7df/test/undocumented/t05_disabled.d.out -------------------------------------------------------------------------------- /test/undocumented/t06_eponymous.d: -------------------------------------------------------------------------------- 1 | // Eponymous templates 2 | 3 | template Bad1() 4 | { 5 | int good11() {}; 6 | 7 | struct good12 {} 8 | 9 | struct Bad1 10 | { 11 | int bad11; 12 | } 13 | } 14 | 15 | /// documented! 16 | struct good1(T) 17 | { 18 | } 19 | 20 | private void good2()() {} 21 | 22 | 23 | /// documented! 24 | template good1() 25 | { 26 | void good1()() {} 27 | } 28 | -------------------------------------------------------------------------------- /test/undocumented/t06_eponymous.d.out: -------------------------------------------------------------------------------- 1 | undocumented/t06_eponymous.d(3,1): Undocumented public declaration: TemplateDeclaration `Bad1()` 2 | undocumented/t06_eponymous.d(11,7): Undocumented public declaration: VarDeclaration `bad11` 3 | --------------------------------------------------------------------------------