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