1
|
|
|
<?php |
2
|
|
|
namespace keeko\tools\command; |
3
|
|
|
|
4
|
|
|
use gossi\codegen\model\PhpClass; |
5
|
|
|
use gossi\codegen\model\PhpMethod; |
6
|
|
|
use gossi\swagger\collections\Definitions; |
7
|
|
|
use gossi\swagger\collections\Paths; |
8
|
|
|
use gossi\swagger\Swagger; |
9
|
|
|
use gossi\swagger\Tag; |
10
|
|
|
use keeko\framework\utils\NameUtils; |
11
|
|
|
use phootwork\file\File; |
12
|
|
|
use phootwork\lang\Text; |
13
|
20 |
|
use Propel\Generator\Model\Table; |
14
|
20 |
|
use Symfony\Component\Console\Input\InputInterface; |
15
|
20 |
|
use Symfony\Component\Console\Output\OutputInterface; |
16
|
20 |
|
|
17
|
|
|
class GenerateApiCommand extends AbstractKeekoCommand { |
18
|
|
|
|
19
|
20 |
|
protected function configure() { |
20
|
20 |
|
$this |
21
|
|
|
->setName('generate:api') |
22
|
|
|
->setDescription('Generates the api for the module') |
23
|
|
|
; |
24
|
|
|
|
25
|
|
|
$this->configureGenerateOptions(); |
26
|
|
|
|
27
|
|
|
parent::configure(); |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Checks whether api can be generated at all by reading composer.json and verify |
32
|
|
|
* all required information are available |
33
|
|
|
*/ |
34
|
|
|
private function preCheck() { |
35
|
|
|
$module = $this->packageService->getModule(); |
36
|
|
|
if ($module === null) { |
37
|
|
|
throw new \DomainException('No module definition found in composer.json - please run `keeko init`.'); |
38
|
|
|
} |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) { |
42
|
|
|
$this->preCheck(); |
43
|
|
|
|
44
|
|
|
// generate api |
45
|
|
|
$api = new File($this->project->getApiFileName()); |
46
|
|
|
|
47
|
|
|
// if generation is forced, generate new API from scratch |
48
|
|
|
if ($input->getOption('force')) { |
49
|
|
|
$swagger = new Swagger(); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
// ... anyway reuse existing one |
53
|
|
|
else { |
54
|
|
|
if (!$api->exists()) { |
55
|
|
|
$api->write('{}'); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$swagger = Swagger::fromFile($this->project->getApiFileName()); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
$module = $this->package->getKeeko()->getModule(); |
62
|
|
|
$swagger->setVersion('2.0'); |
63
|
|
|
$swagger->getInfo()->setTitle($module->getTitle() . ' API'); |
64
|
|
|
$swagger->getTags()->clear(); |
65
|
|
|
$swagger->getTags()->add(new Tag(['name' => $module->getSlug()])); |
66
|
|
|
|
67
|
|
|
$this->generatePaths($swagger); |
68
|
|
|
$this->generateDefinitions($swagger); |
69
|
|
|
|
70
|
|
|
$this->jsonService->write($api->getPathname(), $swagger->toArray()); |
71
|
|
|
$this->io->writeln(sprintf('API for <info>%s</info> written at <info>%s</info>', $this->package->getFullName(), $api->getPathname())); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
// /** |
75
|
|
|
// * Adds the APIModelInterface to package models |
76
|
|
|
// * |
77
|
|
|
// */ |
78
|
|
|
// protected function prepareModels() { |
79
|
|
|
// $models = $this->modelService->getPackageModelNames(); |
80
|
|
|
|
81
|
|
|
// foreach ($models as $modelName) { |
|
|
|
|
82
|
|
|
// $tableName = $this->modelService->getTableName($modelName); |
83
|
|
|
// $model = $this->modelService->getModel($tableName); |
84
|
|
|
// $class = new PhpClass(str_replace('\\\\', '\\', $model->getNamespace() . '\\' . $model->getPhpName())); |
85
|
|
|
// $file = new File($this->codegenService->getFilename($class)); |
86
|
|
|
|
87
|
|
|
// if ($file->exists()) { |
|
|
|
|
88
|
|
|
// $class = PhpClass::fromFile($this->codegenService->getFilename($class)); |
89
|
|
|
// if (!$class->hasInterface('APIModelInterface')) { |
90
|
|
|
// $class->addUseStatement('keeko\\core\\model\\types\\APIModelInterface'); |
91
|
|
|
// $class->addInterface('APIModelInterface'); |
92
|
|
|
// // $typeName = $this->package->getCanonicalName() . '.' . NameUtils::dasherize($modelName); |
93
|
|
|
// // $class->setMethod(PhpMethod::create('getAPIType') |
94
|
|
|
// // ->setBody('return \''.$typeName . '\';') |
95
|
|
|
// // ); |
96
|
|
|
|
97
|
|
|
// $this->codegenService->dumpStruct($class, true); |
|
|
|
|
98
|
|
|
// } |
99
|
|
|
// } |
100
|
|
|
// } |
101
|
|
|
// } |
102
|
|
|
|
103
|
|
|
protected function generatePaths(Swagger $swagger) { |
104
|
|
|
$paths = $swagger->getPaths(); |
105
|
|
|
|
106
|
|
|
foreach ($this->packageService->getModule()->getActionNames() as $name) { |
107
|
|
|
$this->generateOperation($paths, $name); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
protected function generateOperation(Paths $paths, $actionName) { |
112
|
|
|
$this->logger->notice('Generating Operation for: ' . $actionName); |
113
|
|
|
|
114
|
|
|
if (Text::create($actionName)->contains('relationship')) { |
115
|
|
|
$this->generateRelationshipOperation($paths, $actionName); |
116
|
|
|
} else { |
117
|
|
|
$this->generateCRUDOperation($paths, $actionName); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
protected function generateRelationshipOperation(Paths $paths, $actionName) { |
122
|
|
|
$this->logger->notice('Generating Relationship Operation for: ' . $actionName); |
123
|
|
|
$prefix = substr($actionName, 0, strrpos($actionName, 'relationship') + 12); |
124
|
|
|
$module = $this->packageService->getModule(); |
125
|
|
|
|
126
|
|
|
// test for to-many relationship: |
127
|
|
|
$many = $module->hasAction($prefix . '-read') |
128
|
|
|
&& $module->hasAction($prefix . '-update') |
129
|
|
|
&& $module->hasAction($prefix . '-add') |
130
|
|
|
&& $module->hasAction($prefix . '-remove') |
131
|
|
|
; |
132
|
|
|
$single = $module->hasAction($prefix . '-read') |
133
|
|
|
&& $module->hasAction($prefix . '-update') |
134
|
|
|
&& !$many |
135
|
|
|
; |
136
|
|
|
|
137
|
|
|
if (!$many && !$single) { |
138
|
|
|
$this->io->writeln(sprintf('<comment>Couldn\'t detect whether %s is a to-one or to-many relationship, skin generating endpoints</comment>', $actionName)); |
139
|
|
|
return; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
// find model names |
143
|
|
|
$modelName = substr($actionName, 0, strpos($actionName, 'to') - 1); |
144
|
|
|
$start = strpos($actionName, 'to') + 3; |
145
|
|
|
$foreignModelName = substr($actionName, $start, strpos($actionName, 'relationship') - 1 - $start); |
146
|
|
|
|
147
|
|
|
// stop, if one of the models is excluded from api |
148
|
|
|
$codegen = $this->codegenService->getCodegen(); |
149
|
|
|
$excluded = $codegen->getExcludedModels(); |
|
|
|
|
150
|
|
|
if ($excluded->contains($modelName) || $excluded->contains($foreignModelName)) { |
151
|
|
|
return; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
$action = $this->packageService->getAction($actionName); |
155
|
|
|
$type = substr($actionName, strrpos($actionName, '-') + 1); |
156
|
|
|
$method = $this->getMethod($type); |
|
|
|
|
157
|
|
|
$endpoint = '/' . NameUtils::pluralize($modelName) . '/{id}/relationship/' . ($single ? |
158
|
|
|
$foreignModelName : NameUtils::pluralize($foreignModelName)); |
159
|
|
|
|
160
|
|
|
$path = $paths->get($endpoint); |
161
|
|
|
$method = $this->getMethod($type); |
162
|
|
|
$operation = $path->getOperation($method); |
163
|
|
|
$operation->setDescription($action->getTitle()); |
164
|
|
|
$operation->setOperationId($action->getName()); |
165
|
|
|
$operation->getTags()->clear(); |
166
|
|
|
$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug())); |
|
|
|
|
167
|
|
|
|
168
|
|
|
$params = $operation->getParameters(); |
169
|
|
|
$responses = $operation->getResponses(); |
170
|
|
|
|
171
|
|
|
// general model related params |
172
|
|
|
// params |
173
|
|
|
$id = $params->getByName('id'); |
174
|
|
|
$id->setIn('path'); |
175
|
|
|
$id->setDescription(sprintf('The %s id', $modelName)); |
176
|
|
|
$id->setRequired(true); |
177
|
|
|
$id->setType('integer'); |
178
|
|
|
|
179
|
|
|
// response |
180
|
|
|
$invalid = $responses->get('400'); |
181
|
|
|
$invalid->setDescription('Invalid ID supplied'); |
182
|
|
|
|
183
|
|
|
$notfound = $responses->get('404'); |
184
|
|
|
$notfound->setDescription(sprintf('No %s found', $modelName)); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
protected function generateCRUDOperation(Paths $paths, $actionName) { |
188
|
|
|
$this->logger->notice('Generating CRUD Operation for: ' . $actionName); |
189
|
|
|
$database = $this->modelService->getDatabase(); |
190
|
|
|
$action = $this->packageService->getAction($actionName); |
191
|
|
|
$modelName = $this->modelService->getModelNameByAction($action); |
192
|
|
|
$tableName = $this->modelService->getTableName($modelName); |
193
|
|
|
$codegen = $this->codegenService->getCodegen(); |
194
|
|
|
|
195
|
|
|
if (!$database->hasTable($tableName)) { |
196
|
|
|
return; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
if ($codegen->getExcludedModels()->contains($modelName)) { |
|
|
|
|
200
|
|
|
return; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
$type = $this->packageService->getActionType($actionName, $modelName); |
204
|
|
|
$modelObjectName = $database->getTable($tableName)->getPhpName(); |
205
|
|
|
$modelPluralName = NameUtils::pluralize($modelName); |
206
|
|
|
|
207
|
|
|
// find path branch |
208
|
|
|
switch ($type) { |
209
|
|
|
case 'list': |
210
|
|
|
case 'create': |
211
|
|
|
$endpoint = '/' . $modelPluralName; |
212
|
|
|
break; |
213
|
|
|
|
214
|
|
|
case 'read': |
215
|
|
|
case 'update': |
216
|
|
|
case 'delete': |
217
|
|
|
$endpoint = '/' . $modelPluralName . '/{id}'; |
218
|
|
|
break; |
219
|
|
|
|
220
|
|
|
default: |
221
|
|
|
throw new \RuntimeException(sprintf('type (%s) not found, can\'t continue.', $type)); |
222
|
|
|
break; |
|
|
|
|
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
$path = $paths->get($endpoint); |
226
|
|
|
$method = $this->getMethod($type); |
227
|
|
|
$operation = $path->getOperation($method); |
228
|
|
|
$operation->setDescription($action->getTitle()); |
229
|
|
|
$operation->setOperationId($action->getName()); |
230
|
|
|
$operation->getTags()->clear(); |
231
|
|
|
$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug())); |
|
|
|
|
232
|
|
|
|
233
|
|
|
$params = $operation->getParameters(); |
234
|
|
|
$responses = $operation->getResponses(); |
235
|
|
|
|
236
|
|
|
switch ($type) { |
237
|
|
|
case 'list': |
238
|
|
|
$ok = $responses->get('200'); |
239
|
|
|
$ok->setDescription(sprintf('Array of %s', $modelPluralName)); |
240
|
|
|
$ok->getSchema()->setRef('#/definitions/' . 'Paged' . NameUtils::pluralize($modelObjectName)); |
241
|
|
|
break; |
242
|
|
|
|
243
|
|
|
case 'create': |
244
|
|
|
// params |
245
|
|
|
$body = $params->getByName('body'); |
246
|
|
|
$body->setName('body'); |
247
|
|
|
$body->setIn('body'); |
248
|
|
|
$body->setDescription(sprintf('The new %s', $modelName)); |
249
|
|
|
$body->setRequired(true); |
250
|
|
|
$body->getSchema()->setRef('#/definitions/Writable' . $modelObjectName); |
251
|
|
|
|
252
|
|
|
// response |
253
|
|
|
$ok = $responses->get('201'); |
254
|
|
|
$ok->setDescription(sprintf('%s created', $modelName)); |
255
|
|
|
break; |
256
|
|
|
|
257
|
|
|
case 'read': |
258
|
|
|
// response |
259
|
|
|
$ok = $responses->get('200'); |
260
|
|
|
$ok->setDescription(sprintf('gets the %s', $modelName)); |
261
|
|
|
$ok->getSchema()->setRef('#/definitions/' . $modelObjectName); |
262
|
|
|
break; |
263
|
|
|
|
264
|
|
|
case 'update': |
265
|
|
|
// response |
266
|
|
|
$ok = $responses->get('200'); |
267
|
|
|
$ok->setDescription(sprintf('%s updated', $modelName)); |
268
|
|
|
$ok->getSchema()->setRef('#/definitions/' . $modelObjectName); |
269
|
|
|
break; |
270
|
|
|
|
271
|
|
|
case 'delete': |
272
|
|
|
// response |
273
|
|
|
$ok = $responses->get('204'); |
274
|
|
|
$ok->setDescription(sprintf('%s deleted', $modelName)); |
275
|
|
|
break; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
if ($type == 'read' || $type == 'update' || $type == 'delete') { |
279
|
|
|
// params |
280
|
|
|
$id = $params->getByName('id'); |
281
|
|
|
$id->setIn('path'); |
282
|
|
|
$id->setDescription(sprintf('The %s id', $modelName)); |
283
|
|
|
$id->setRequired(true); |
284
|
|
|
$id->setType('integer'); |
285
|
|
|
|
286
|
|
|
// response |
287
|
|
|
$invalid = $responses->get('400'); |
288
|
|
|
$invalid->setDescription('Invalid ID supplied'); |
289
|
|
|
|
290
|
|
|
$notfound = $responses->get('404'); |
291
|
|
|
$notfound->setDescription(sprintf('No %s found', $modelName)); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
// response - @TODO Error model |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
private function getMethod($type) { |
298
|
|
|
$methods = [ |
299
|
|
|
'list' => 'get', |
300
|
|
|
'create' => 'post', |
301
|
|
|
'read' => 'get', |
302
|
|
|
'update' => 'patch', |
303
|
|
|
'delete' => 'delete', |
304
|
|
|
'add' => 'post', |
305
|
|
|
'remove' => 'delete' |
306
|
|
|
]; |
307
|
|
|
|
308
|
|
|
return $methods[$type]; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
protected function generateDefinitions(Swagger $swagger) { |
312
|
|
|
$definitions = $swagger->getDefinitions(); |
313
|
|
|
|
314
|
|
|
// general definitions |
315
|
|
|
$this->generatePagedMeta($definitions); |
316
|
|
|
$this->generateResourceIdentifier($definitions); |
317
|
|
|
|
318
|
|
|
// models |
319
|
|
|
$modelName = $this->modelService->getModelName(); |
320
|
|
|
if ($modelName !== null) { |
321
|
|
|
$this->generateDefinition($definitions, $modelName); |
|
|
|
|
322
|
|
|
} else { |
323
|
|
|
foreach ($this->modelService->getModels() as $model) { |
324
|
|
|
$this->generateDefinition($definitions, $model); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
protected function generatePagedMeta(Definitions $definitions) { |
330
|
|
|
$props = $definitions->get('PagedMeta')->setType('object')->getProperties(); |
331
|
|
|
$names = ['total', 'first', 'next', 'previous', 'last']; |
332
|
|
|
|
333
|
|
|
foreach ($names as $name) { |
334
|
|
|
$props->get($name)->setType('integer'); |
335
|
|
|
} |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
protected function generateResourceIdentifier(Definitions $definitions) { |
339
|
|
|
$props = $definitions->get('ResourceIdentifier')->setType('object')->getProperties(); |
340
|
|
|
$this->generateIdentifier($props); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
protected function generateIdentifier(Definitions $props) { |
344
|
|
|
$props->get('id')->setType('string'); |
345
|
|
|
$props->get('type')->setType('string'); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
protected function generateResourceData(Definitions $props) { |
|
|
|
|
349
|
|
|
$data = $props->get('data')->setType('object')->getProperties(); |
350
|
|
|
$this->generateIdentifier($data); |
351
|
|
|
return $data; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
protected function generateDefinition(Definitions $definitions, Table $model) { |
355
|
|
|
$this->logger->notice('Generating Definition for: ' . $model->getOriginCommonName()); |
356
|
|
|
$modelObjectName = $model->getPhpName(); |
357
|
|
|
$codegen = $this->codegenService->getCodegen(); |
358
|
|
|
|
359
|
|
|
// stop if model is excluded |
360
|
|
|
if ($codegen->getExcludedModels()->contains($model->getOriginCommonName())) { |
|
|
|
|
361
|
|
|
return; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
// paged model |
365
|
|
|
$pagedModel = 'Paged' . NameUtils::pluralize($modelObjectName); |
366
|
|
|
$paged = $definitions->get($pagedModel)->setType('object')->getProperties(); |
367
|
|
|
$paged->get('data') |
368
|
|
|
->setType('array') |
369
|
|
|
->getItems()->setRef('#/definitions/' . $modelObjectName); |
370
|
|
|
$paged->get('meta')->setRef('#/definitions/PagedMeta'); |
371
|
|
|
|
372
|
|
|
// writable model |
373
|
|
|
$writable = $definitions->get('Writable' . $modelObjectName)->setType('object')->getProperties(); |
374
|
|
|
$this->generateModelProperties($writable, $model, true); |
375
|
|
|
|
376
|
|
|
// readable model |
377
|
|
|
$readable = $definitions->get($modelObjectName)->setType('object')->getProperties(); |
378
|
|
|
$this->generateModelProperties($readable, $model, false); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
protected function generateModelProperties(Definitions $props, Table $model, $write = false) { |
382
|
|
|
// links |
383
|
|
|
if (!$write) { |
384
|
|
|
$links = $props->get('links')->setType('object')->getProperties(); |
385
|
|
|
$links->get('self')->setType('string'); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
// data |
389
|
|
|
$data = $this->generateResourceData($props); |
390
|
|
|
|
391
|
|
|
// attributes |
392
|
|
|
$attrs = $data->get('attributes'); |
393
|
|
|
$attrs->setType('object'); |
394
|
|
|
$this->generateModelAttributes($attrs->getProperties(), $model, $write); |
395
|
|
|
|
396
|
|
|
// relationships |
397
|
|
|
if ($this->hasRelationships($model)) { |
398
|
|
|
$relationships = $data->get('relationships')->setType('object')->getProperties(); |
399
|
|
|
$this->generateModelRelationships($relationships, $model, $write); |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
protected function generateModelAttributes(Definitions $props, Table $model, $write = false) { |
404
|
|
|
$modelName = $model->getOriginCommonName(); |
405
|
|
|
$filter = $write |
406
|
|
|
? $this->codegenService->getCodegen()->getWriteFilter($modelName) |
407
|
|
|
: $this->codegenService->getCodegen()->getReadFilter($modelName); |
408
|
|
|
|
409
|
|
|
if ($write) { |
410
|
|
|
$filter = array_merge($filter, $this->codegenService->getComputedFields($model)); |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
// no id, already in identifier |
414
|
|
|
$filter[] = 'id'; |
415
|
|
|
$types = ['int' => 'integer']; |
416
|
|
|
|
417
|
|
|
foreach ($model->getColumns() as $col) { |
418
|
|
|
$prop = $col->getName(); |
419
|
|
|
|
420
|
|
|
if (!in_array($prop, $filter)) { |
421
|
|
|
$type = $col->getPhpType(); |
422
|
|
|
if (isset($types[$type])) { |
423
|
|
|
$type = $types[$type]; |
424
|
|
|
} |
425
|
|
|
$props->get($prop)->setType($type); |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
return $props; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
protected function hasRelationships(Table $model) { |
433
|
|
|
return (count($model->getForeignKeys()) + count($model->getCrossFks())) > 0; |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
protected function generateModelRelationships(Definitions $props, Table $model, $write = false) { |
437
|
|
|
$relationships = $this->modelService->getRelationships($model); |
438
|
|
|
|
439
|
|
|
// to-one |
440
|
|
|
foreach ($relationships->getOne() as $one) { |
441
|
|
|
$typeName = $one->getRelatedTypeName(); |
442
|
|
|
$rel = $props->get($typeName)->setType('object')->getProperties(); |
443
|
|
|
|
444
|
|
|
// links |
445
|
|
|
if (!$write) { |
446
|
|
|
$links = $rel->get('links')->setType('object')->getProperties(); |
447
|
|
|
$links->get('self')->setType('string'); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
// data |
451
|
|
|
$this->generateResourceData($rel); |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
// to-many |
455
|
|
|
foreach ($relationships->getMany() as $many) { |
456
|
|
|
$typeName = $many->getRelatedTypeName(); |
457
|
|
|
$rel = $props->get($typeName)->setType('object')->getProperties(); |
458
|
|
|
|
459
|
|
|
// links |
460
|
|
|
if (!$write) { |
461
|
|
|
$links = $rel->get('links')->setType('object')->getProperties(); |
462
|
|
|
$links->get('self')->setType('string'); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
// data |
466
|
|
|
$rel->get('data') |
467
|
|
|
->setType('array') |
468
|
|
|
->getItems()->setRef('#/definitions/ResourceIdentifier'); |
469
|
|
|
} |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
} |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.