1 // Distributed under the Boost Software License, Version 1.0. 2 // (See accompanying file LICENSE_1_0.txt or copy at 3 // http://www.boost.org/LICENSE_1_0.txt) 4 5 module dscanner.analysis.has_public_example; 6 7 import dscanner.analysis.base; 8 import dsymbol.scope_ : Scope; 9 import dparse.ast; 10 import dparse.lexer; 11 12 import std.algorithm; 13 import std.stdio; 14 15 /** 16 * Checks for public declarations without a documented unittests. 17 * For now, variable and enum declarations aren't checked. 18 */ 19 final class HasPublicExampleCheck : BaseAnalyzer 20 { 21 alias visit = BaseAnalyzer.visit; 22 23 mixin AnalyzerInfo!"has_public_example"; 24 25 this(string fileName, const(Scope)* sc, bool skipTests = false) 26 { 27 super(fileName, sc, skipTests); 28 } 29 30 override void visit(const Module mod) 31 { 32 // the last seen declaration is memorized 33 Declaration lastDecl; 34 35 // keep track of ddoced unittests after visiting lastDecl 36 bool hasNoDdocUnittest; 37 38 // on lastDecl reset we check for seen ddoced unittests since lastDecl was observed 39 void checkLastDecl() 40 { 41 if (lastDecl !is null && hasNoDdocUnittest) 42 triggerError(lastDecl); 43 lastDecl = null; 44 } 45 46 // check all public top-level declarations 47 foreach (decl; mod.declarations) 48 { 49 if (decl.attributes.any!(a => a.deprecated_ !is null)) 50 { 51 lastDecl = null; 52 continue; 53 } 54 55 if (!isPublic(decl.attributes)) 56 { 57 checkLastDecl(); 58 continue; 59 } 60 61 const bool hasDdocHeader = hasDdocHeader(decl); 62 63 // check the documentation of a unittest declaration 64 if (decl.unittest_ !is null) 65 { 66 if (hasDdocHeader) 67 hasNoDdocUnittest = false; 68 } 69 // add all declarations that could be publicly documented to the lastDecl "stack" 70 else if (hasDittableDecl(decl)) 71 { 72 // ignore dittoed declarations 73 if (hasDittos(decl)) 74 continue; 75 76 // new public symbol -> check the previous decl 77 checkLastDecl; 78 79 lastDecl = hasDdocHeader ? cast(Declaration) decl : null; 80 hasNoDdocUnittest = true; 81 } 82 else 83 // ran into variableDeclaration or something else -> reset & validate current lastDecl "stack" 84 checkLastDecl; 85 } 86 checkLastDecl; 87 } 88 89 private: 90 91 bool hasDitto(Decl)(const Decl decl) 92 { 93 import ddoc.comments : parseComment; 94 if (decl is null || decl.comment is null) 95 return false; 96 97 return parseComment(decl.comment, null).isDitto; 98 } 99 100 bool hasDittos(Decl)(const Decl decl) 101 { 102 foreach (property; possibleDeclarations) 103 if (mixin("hasDitto(decl." ~ property ~ ")")) 104 return true; 105 return false; 106 } 107 108 bool hasDittableDecl(Decl)(const Decl decl) 109 { 110 foreach (property; possibleDeclarations) 111 if (mixin("decl." ~ property ~ " !is null")) 112 return true; 113 return false; 114 } 115 116 import std.meta : AliasSeq; 117 alias possibleDeclarations = AliasSeq!( 118 "classDeclaration", 119 "enumDeclaration", 120 "functionDeclaration", 121 "interfaceDeclaration", 122 "structDeclaration", 123 "templateDeclaration", 124 "unionDeclaration", 125 //"variableDeclaration", 126 ); 127 128 bool hasDdocHeader(const Declaration decl) 129 { 130 if (decl.declarations !is null) 131 return false; 132 133 // unittest can have ddoc headers as well, but don't have a name 134 if (decl.unittest_ !is null && decl.unittest_.comment.ptr !is null) 135 return true; 136 137 foreach (property; possibleDeclarations) 138 if (mixin("decl." ~ property ~ " !is null && decl." ~ property ~ ".comment.ptr !is null")) 139 return true; 140 141 return false; 142 } 143 144 bool isPublic(const Attribute[] attrs) 145 { 146 import dparse.lexer : tok; 147 148 enum tokPrivate = tok!"private", tokProtected = tok!"protected", tokPackage = tok!"package"; 149 150 if (attrs.map!`a.attribute`.any!(x => x == tokPrivate || x == tokProtected || x == tokPackage)) 151 return false; 152 153 return true; 154 } 155 156 void triggerError(const Declaration decl) 157 { 158 foreach (property; possibleDeclarations) 159 if (auto fn = mixin("decl." ~ property)) 160 addMessage(fn.name.line, fn.name.column, fn.name.text); 161 } 162 163 void addMessage(size_t line, size_t column, string name) 164 { 165 import std..string : format; 166 167 addErrorMessage(line, column, "dscanner.style.has_public_example", name is null 168 ? "Public declaration has no documented example." 169 : format("Public declaration '%s' has no documented example.", name)); 170 } 171 } 172 173 unittest 174 { 175 import std.stdio : stderr; 176 import std.format : format; 177 import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 178 import dscanner.analysis.helpers : assertAnalyzerWarnings; 179 180 StaticAnalysisConfig sac = disabledConfig(); 181 sac.has_public_example = Check.enabled; 182 183 assertAnalyzerWarnings(q{ 184 /// C 185 class C{} 186 /// 187 unittest {} 188 189 /// I 190 interface I{} 191 /// 192 unittest {} 193 194 /// e 195 enum e = 0; 196 /// 197 unittest {} 198 199 /// f 200 void f(){} 201 /// 202 unittest {} 203 204 /// S 205 struct S{} 206 /// 207 unittest {} 208 209 /// T 210 template T(){} 211 /// 212 unittest {} 213 214 /// U 215 union U{} 216 /// 217 unittest {} 218 }, sac); 219 220 // enums or variables don't need to have public unittest 221 assertAnalyzerWarnings(q{ 222 /// C 223 class C{} // [warn]: Public declaration 'C' has no documented example. 224 unittest {} 225 226 /// I 227 interface I{} // [warn]: Public declaration 'I' has no documented example. 228 unittest {} 229 230 /// f 231 void f(){} // [warn]: Public declaration 'f' has no documented example. 232 unittest {} 233 234 /// S 235 struct S{} // [warn]: Public declaration 'S' has no documented example. 236 unittest {} 237 238 /// T 239 template T(){} // [warn]: Public declaration 'T' has no documented example. 240 unittest {} 241 242 /// U 243 union U{} // [warn]: Public declaration 'U' has no documented example. 244 unittest {} 245 }, sac); 246 247 // test module header unittest 248 assertAnalyzerWarnings(q{ 249 unittest {} 250 /// C 251 class C{} // [warn]: Public declaration 'C' has no documented example. 252 }, sac); 253 254 // test documented module header unittest 255 assertAnalyzerWarnings(q{ 256 /// 257 unittest {} 258 /// C 259 class C{} // [warn]: Public declaration 'C' has no documented example. 260 }, sac); 261 262 // test multiple unittest blocks 263 assertAnalyzerWarnings(q{ 264 /// C 265 class C{} // [warn]: Public declaration 'C' has no documented example. 266 unittest {} 267 unittest {} 268 unittest {} 269 270 /// U 271 union U{} 272 unittest {} 273 /// 274 unittest {} 275 unittest {} 276 }, sac); 277 278 /// check private 279 assertAnalyzerWarnings(q{ 280 /// C 281 private class C{} 282 283 /// I 284 protected interface I{} 285 286 /// e 287 package enum e = 0; 288 289 /// f 290 package(std) void f(){} 291 292 /// S 293 extern(C) struct S{} 294 /// 295 unittest {} 296 }, sac); 297 298 // check intermediate private declarations 299 // removed for issue #500 300 /*assertAnalyzerWarnings(q{ 301 /// C 302 class C{} 303 private void foo(){} 304 /// 305 unittest {} 306 }, sac);*/ 307 308 // check intermediate ditto-ed declarations 309 assertAnalyzerWarnings(q{ 310 /// I 311 interface I{} 312 /// ditto 313 void f(){} 314 /// 315 unittest {} 316 }, sac); 317 318 // test reset on private symbols (#500) 319 assertAnalyzerWarnings(q{ 320 /// 321 void dirName(C)(C[] path) {} // [warn]: Public declaration 'dirName' has no documented example. 322 private void _dirName(R)(R path) {} 323 /// 324 unittest {} 325 }, sac); 326 327 // deprecated symbols shouldn't require a test 328 assertAnalyzerWarnings(q{ 329 /// 330 deprecated void dirName(C)(C[] path) {} 331 }, sac); 332 333 stderr.writeln("Unittest for HasPublicExampleCheck passed."); 334 } 335