Completed
Push — master ( d1fe82...d3deac )
by Amine
11s
created

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php namespace Tarsana\Functional;
2
/**
3
 * This script parses the source files using [dox](https://github.com/tj/dox)
4
 * and generates the unit tests and documentation files.
5
 */
6
require __DIR__ . '/vendor/autoload.php';
7
8
/**
9
 * Custom Types:
10
 *  DoxBlock :: {
11
 *     tags: [{
12
 *         type: String,
13
 *         string: String,
14
 *         types: [String],
15
 *         name: String,
16
 *         description: String
17
 *         ...
18
 *     }],
19
 *     description: {
20
 *         full: String,
21
 *         summary: String,
22
 *         body: String
23
 *     },
24
 *     code: String,
25
 *     ctx: {
26
 *         type: String,
27
 *         name: String,
28
 *         ...
29
 *     }
30
 *     isPrivate:
31
 *     isEvent:
32
 *     isConstructor:
33
 *     line:
34
 *     ignore:
35
 *  }
36
 *
37
 * Block :: {
38
 *     type: file|function|class|method
39
 *     name: String // DoxBlock.ctx.name
40
 *     params: [{type: String, name: String}]
41
 *     return: String
42
 *     signatures: [String]
43
 *     description: String
44
 *     summary: String
45
 *     internal: Boolean
46
 *     ignore: Boolean
47
 *     code: String
48
 * }
49
 *
50
 * Operation :: {
51
 *     name: String,
52
 *     signature: String
53
 * }
54
 *
55
 * Module :: {
56
 *     path: String
57
 *     name: String
58
 *     docsPath: String
59
 *     testsPath: String
60
 *     blocks: [Block]
61
 *     docs: String
62
 *     tests: String
63
 *     testsFooter: String
64
 *     streamOperations: String
65
 *     streamMethods: String
66
 * }
67
 */
68
69
/**
70
 * The entry point.
71
 *
72
 * @signature [String] -> IO
73
 * @param  array $modules
74
 * @return void
75
 */
76
function build_main($modules) {
77
    build_init_stream_operations();
78
    each(_f('build_module'), $modules);
79
    build_close_stream_operations();
80
}
81
82
/**
83
 * Writes the header of the stream operations file.
84
 *
85
 * @signature IO
86
 * @return void
87
 */
88
function build_init_stream_operations() {
89
    file_put_contents(
90
        'src/Internal/_stream_operations.php',
91
        "<?php\n\nuse Tarsana\Functional as F;\n\nreturn F\map(F\apply(F\_f('_stream_operation')), [\n\t['then', 'Function -> Any -> Any', F\_f('_stream_then')],\n"
92
    );
93
    file_put_contents(
94
        'docs/stream-operations.md',
95
        "# Stream Operations"
96
    );
97
}
98
99
/**
100
 * Writes the footer of the stream operations file.
101
 *
102
 * @signature IO
103
 * @return void
104
 */
105
function build_close_stream_operations() {
106
    file_put_contents(
107
        'src/Internal/_stream_operations.php',
108
        "\n]);\n", FILE_APPEND
109
    );
110
}
111
112
/**
113
 * Extracts the modules files from composer.json.
114
 *
115
 * @signature [String]
116
 * @return array
117
 */
118
function get_modules() {
119
    $composer = json_decode(file_get_contents(__DIR__.'/composer.json'));
120
    return $composer->autoload->files;
121
}
122
123
/**
124
 * Generates unit tests and documentation for a module.
125
 *
126
 * @signature String -> IO
127
 * @param  string $path
128
 * @return void
129
 */
130
function build_module($path) {
131
    apply(process_of([
132
        'module_of',
133
        'generate_docs',
134
        'generate_tests',
135
        'generate_stream_operations',
136
        'generate_stream_methods',
137
        'write_module'
138
    ]), [$path]);
139
}
140
141
/**
142
 * Writes the module's docs and tests.
143
 *
144
 * @signature Module -> IO
145
 * @param  object $module
146
 * @return void
147
 */
148
function write_module($module) {
149 View Code Duplication
    if ($module->docs) {
150
        $docsDir  = dirname($module->docsPath);
151
        if (!is_dir($docsDir))
152
            mkdir($docsDir, 0777, true);
153
        file_put_contents($module->docsPath,  $module->docs);
154
    }
155 View Code Duplication
    if ($module->tests) {
156
        $testsDir = dirname($module->testsPath);
157
        if (!is_dir($testsDir))
158
            mkdir($testsDir, 0777, true);
159
        file_put_contents($module->testsPath, $module->tests);
160
    }
161
    if ($module->streamOperations) {
162
        file_put_contents('src/Internal/_stream_operations.php', $module->streamOperations, FILE_APPEND);
163
    }
164
    if ($module->streamMethods) {
165
        file_put_contents('docs/stream-operations.md', $module->streamMethods, FILE_APPEND);
166
    }
167
}
168
169
/**
170
 * Creates a module from a path.
171
 *
172
 * @signature String -> Module
173
 * @param  string $path
174
 * @return object
175
 */
176
function module_of($path) {
177
    return apply(process_of([
178
        'fill_name',
179
        'fill_docs_path',
180
        'fill_tests_path',
181
        'fill_blocks'
182
    ]), [(object)['path' => $path]]);
183
}
184
185
/**
186
 * Fills documentation file path based on source file path.
187
 * 'src/xxx.php' -> 'docs/xxx.md'
188
 *
189
 * @signature Module -> Module
190
 * @param  object $module
191
 * @return object
192
 */
193
function fill_docs_path($module) {
194
    $module->docsPath = replace(['src', '.php'], ['docs', '.md'], $module->path);
195
    return $module;
196
}
197
198
/**
199
 * Fills tests file path based on source file path.
200
 * 'src/xxx.php' -> 'tests/xxxTest.php'
201
 *
202
 * @signature Module -> Module
203
 * @param  object $module
204
 * @return object
205
 */
206
function fill_tests_path($module) {
207
    $name = ucfirst(camelCase($module->name));
208
    $dir = 'tests' . remove(3, dirname($module->path));
209
    $module->testsPath = "{$dir}/{$name}Test.php";
210
    return $module;
211
}
212
213
/**
214
 * Fills the name of the Module based on the path.
215
 * 'src/xxx/aaa.php' -> 'aaa'
216
 *
217
 * @signature Module -> Module
218
 * @param  object $module
219
 * @return object
220
 */
221
function fill_name($module) {
222
    $module->name = apply(pipe(split('/'), last(), split('.'), head()), [$module->path]);
223
    return $module;
224
}
225
226
/**
227
 * Fills the blocks of the Module based on the path.
228
 *
229
 * @signature Module -> Module
230
 * @param  array $module
231
 * @return array
232
 */
233
function fill_blocks($module) {
234
    $module->blocks = apply(pipe(
235
        prepend('dox -r < '), // "dox -r < src/...php"
236
        'shell_exec',         // "[{...}, ...]"
237
        'json_decode',        // [DoxBlock]
238
        map(_f('make_block'))
239
        // sort()
240
    ), [$module->path]);
241
    return $module;
242
}
243
244
/**
245
 * Converts a DoxBlock to a Block.
246
 *
247
 * @signature DoxBlock -> Block
248
 * @param  object $doxBlock
249
 * @return object
250
 */
251
function make_block($doxBlock) {
252
    $tags = groupBy(get('name'), tags_of($doxBlock));
253
254
    $type = 'function';
255
    if (has('file', $tags)) $type = 'file';
256
    if (has('class', $tags)) $type = 'class';
257
    if (has('method', $tags)) $type = 'method';
258
259
    $params = map(function($tag){
260
        $parts = split(' ', get('value', $tag));
261
        return [
262
            'type' => $parts[0],
263
            'name' => $parts[1]
264
        ];
265
    }, get('param', $tags) ?: []);
266
267
    $return = getPath(['return', 0, 'value'], $tags);
268
    $signatures = get('signature', $tags);
269
    if ($signatures)
270
        $signatures = map(get('value'), $signatures);
271
    return (object) [
272
        'type' => $type,
273
        'name' => getPath(['ctx', 'name'], $doxBlock),
274
        'params' => $params,
275
        'return' => $return,
276
        'signatures' => $signatures,
277
        'description' => getPath(['description', 'full'], $doxBlock),
278
        'summary' => getPath(['description', 'summary'], $doxBlock),
279
        'internal' => has('internal', $tags),
280
        'ignore' => has('ignore', $tags),
281
        'stream' => has('stream', $tags)
282
        // 'code' => get('code', $doxBlock)
283
    ];
284
}
285
286
/**
287
 * Returns an array of tags, each having a name and a value.
288
 *
289
 * @signature DoxBlock -> [{name: String, value: String}]
290
 * @param  object $doxBlock
291
 * @return array
292
 */
293
function tags_of($doxBlock) {
294
    if ($doxBlock->tags)
295
        return map(function($tag){
296
            return (object) [
297
                'name'  => $tag->type,
298
                'value' => $tag->string
299
            ];
300
        }, $doxBlock->tags);
301
    return [];
302
}
303
304
/**
305
 * Generates documentation contents for a module.
306
 *
307
 * @signature Module -> Module
308
 * @param  object $module
309
 * @return object
310
 */
311
function generate_docs($module) {
312
    $module->docs = '';
313
    if (startsWith('_', $module->name))
314
        return $module;
315
    return apply(process_of([
316
        'generate_docs_header',
317
        'generate_docs_sommaire',
318
        'generate_docs_contents'
319
    ]), [$module]);
320
}
321
322
/**
323
 * Generates documentation header.
324
 *
325
 * @signature Module -> Module
326
 * @param  object $module
327
 * @return object
328
 */
329
function generate_docs_header($module) {
330
    $name = $module->name;
331
    $description = get('description', head($module->blocks));
332
    $module->docs .= "#{$name}\n\n{$description}\n\n";
333
    return $module;
334
}
335
336
/**
337
 * Generates documentation table of contents.
338
 *
339
 * @signature Module -> Module
340
 * @param  object $module
341
 * @return object
342
 */
343 View Code Duplication
function generate_docs_sommaire($module) {
344
    $blocks = filter (
345
        satisfiesAll(['ignore' => not(), 'internal' => not(), 'type' => equals('function')]),
346
        $module->blocks
347
    );
348
    $items = map(_f('generate_docs_sommaire_item'), $blocks);
349
    $module->docs .= join('', $items);
350
    return $module;
351
}
352
353
/**
354
 * Generates an item of the documentation's table of contents.
355
 *
356
 * @signature Block -> String
357
 * @param  object $block
358
 * @return string
359
 */
360
function generate_docs_sommaire_item($block) {
361
    $title = get('name', $block);
362
    $link  = lowerCase($title);
363
    return "- [{$title}](#{$link}) - {$block->summary}\n\n";
364
}
365
366
/**
367
 * Generates documentation contents.
368
 *
369
 * @signature Module -> Module
370
 * @param  object $module
371
 * @return object
372
 */
373 View Code Duplication
function generate_docs_contents($module) {
374
    $blocks = filter (
375
        satisfiesAll(['ignore' => not(), 'internal' => not()]),
376
        $module->blocks
377
    );
378
    $contents = map(_f('generate_docs_contents_item'), $blocks);
379
    $module->docs .= join('', $contents);
380
    return $module;
381
}
382
383
/**
384
 * Generates an item of the documentation's contents.
385
 *
386
 * @signature Block -> String
387
 * @param  object $block
388
 * @return string
389
 */
390
function generate_docs_contents_item($block) {
391
    if ($block->type != 'function')
392
        return '';
393
    $params = join(', ', map(pipe(values(), join(' ')), get('params', $block)));
394
    $return = get('return', $block);
395
    $prototype = "```php\n{$block->name}({$params}) : {$return}\n```\n\n";
396
    $signature = '';
397
    $blockSignature = join("\n", $block->signatures);
398
    if ($blockSignature)
399
        $signature = "```\n{$blockSignature}\n```\n\n";
400
    return "# {$block->name}\n\n{$prototype}{$signature}{$block->description}\n\n";
401
}
402
403
/**
404
 * Generates tests contents for a module.
405
 *
406
 * @signature Module -> Module
407
 * @param  object $module
408
 * @return object
409
 */
410
function generate_tests($module) {
411
    $module->tests = '';
412
    $module->testsFooter = '';
413
    return apply(process_of([
414
        'generate_tests_header',
415
        'generate_tests_contents',
416
        'generate_tests_footer'
417
    ]), [$module]);
418
}
419
420
/**
421
 * Generates module's tests header.
422
 *
423
 * @signature Module -> Module
424
 * @param  object $module
425
 * @return object
426
 */
427
function generate_tests_header($module) {
428
    $namespace = "Tarsana\UnitTests\Functional";
429
    $additionalNamespace = replace("/", "\\", remove(6, dirname($module->testsPath)));
430
    if ($additionalNamespace)
431
        $namespace .= "\\" . $additionalNamespace;
432
    $name = remove(-4, last(split("/", $module->testsPath)));
433
    $module->tests .= "<?php namespace {$namespace};\n\nuse Tarsana\Functional as F;\n\nclass {$name} extends \Tarsana\UnitTests\Functional\UnitTest {\n";
434
    return $module;
435
}
436
437
/**
438
 * Generates module's tests contents.
439
 *
440
 * @signature Module -> Module
441
 * @param  object $module
442
 * @return object
443
 */
444
function generate_tests_contents($module) {
445
    $blocks = filter (
446
        satisfiesAll(['ignore' => not()]),
447
        $module->blocks
448
    );
449
    $contents = join("\n", map(function($block) use($module) {
450
        return generate_tests_contents_item($block, $module);
451
    }, $blocks));
452
    if (trim($contents) != '')
453
        $module->tests .= $contents;
454
    else
455
        $module->tests = '';
456
    return $module;
457
}
458
459
/**
460
 * Generates a test for a module.
461
 *
462
 * @signature Block -> Module -> String
463
 * @param  object $block
464
 * @param  object $module
465
 * @return string
466
 */
467
function generate_tests_contents_item($block, $module) {
468
    if ($block->type != 'function')
469
        return '';
470
471
    $code = apply(pipe(
472
        _f('code_from_description'),
473
        chunks("\"\"''{}[]()", "\n"),
474
        map(function($part) use($module) {
475
            return add_assertions($part, $module);
476
        }),
477
        filter(pipe('trim', notEq(''))),
478
        chain(split("\n")),
479
        map(prepend("\t\t")),
480
        join("\n")
481
    ), [$block]);
482
483
    if ('' == trim($code))
484
        return '';
485
    return prepend("\tpublic function test_{$block->name}() {\n",
486
        append("\n\t}\n", $code)
487
    );
488
}
489
490
/**
491
 * Extracts the code snippet from the description of a block.
492
 *
493
 * @signature Block -> String
494
 * @param  object $block
495
 * @return string
496
 */
497
function code_from_description($block) {
498
    $description = get('description', $block);
499
    if (!contains('```php', $description))
500
        return '';
501
    $code = remove(7 + indexOf('```php', $description), $description);
502
    return remove(-4, trim($code));
503
}
504
505
/**
506
 * Adds assertions to a part of the code.
507
 *
508
 * @signature String -> String
509
 * @param  string $part
510
 * @return string
511
 */
512
function add_assertions($part, $module) {
513
    if (contains('; //=> ', $part)) {
514
        $pieces = split('; //=> ', $part);
515
        $part = "\$this->assertEquals({$pieces[1]}, {$pieces[0]});";
516
    }
517
    elseif (contains('; // throws ', $part)) {
518
        $pieces = split('; // throws ', $part);
519
        $variables = match('/ \$[0-9a-zA-Z_]+/', $pieces[0]);
520
        $use = '';
521
        if (length($variables)) {
522
            $variables = join(', ', map('trim', $variables));
523
            $use = "use({$variables}) ";
524
        }
525
        return "\$this->assertErrorThrown(function() {$use}{\n\t$pieces[0]; \n},\n{$pieces[1]});";
526
    }
527
    elseif (startsWith('class ', $part) || startsWith('function ', $part)) {
528
        $module->testsFooter .= $part . "\n\n";
529
        $part = '';
530
    }
531
    return $part;
532
}
533
534
/**
535
 * Generates module's tests footer.
536
 *
537
 * @signature Module -> Module
538
 * @param  object $module
539
 * @return object
540
 */
541
function generate_tests_footer($module) {
542
    if ($module->tests)
543
        $module->tests .= "}\n\n{$module->testsFooter}";
544
    return $module;
545
}
546
547
/**
548
 * Generates module's stream operations.
549
 *
550
 * @signature Module -> Module
551
 * @param  array $module
552
 * @return array
553
 */
554
function generate_stream_operations($module) {
555
    $blocks = filter (
556
        satisfiesAll(['ignore' => equals(false), 'stream' => equals(true)]),
557
        $module->blocks
558
    );
559
    $operations = map(_f('stream_operation_declaration'), chain(_f('stream_operations_of_block'), $blocks));
560
    $module->streamOperations = join("", $operations);
561
    return $module;
562
}
563
564
/**
565
 * Gets stream operations from a block.
566
 *
567
 * @signature Block -> [Operation]
568
 * @param  object $block
569
 * @return string
570
 */
571
function stream_operations_of_block($block) {
572
    return map(function($signature) use($block) {
573
        return (object) [
574
            'name' => $block->name,
575
            'signature' => normalize_signature($signature)
576
        ];
577
    }, get('signatures', $block));
578
}
579
580
/**
581
 * Converts a formal signature to a stream signature.
582
 * [a]       becomes List
583
 * {k: v}    becomes Array|Object
584
 * (a -> b)  becomes Function
585
 *  *        becomes Any
586
 *
587
 * @signature String -> String
588
 * @param  string $signature
589
 * @return string
590
 */
591
function normalize_signature($signature) {
592
    // This is not the best way to do it :P
593
    return join(' -> ', map(pipe(
594
        regReplace('/Maybe\([a-z][^\)]*\)/', 'Any'),
595
        regReplace('/Maybe\(([^\)]+)\)/', '$1|Null'),
596
        regReplace('/\([^\)]+\)/', 'Function'),
597
        regReplace('/\[[^\]]+\]/', 'List'),
598
        regReplace('/\{[^\}]+\}/', 'Object|Array'),
599
        regReplace('/^.$/', 'Any'),
600
        regReplace('/[\(\)\[\]\{\}]/', '')
601
    ), chunks('(){}', ' -> ', $signature)));
602
}
603
604
/**
605
 * Converts a stream operation to declaration array.
606
 *
607
 * @signature Operation -> String
608
 * @param  object $operation
609
 * @return string
610
 */
611
function stream_operation_declaration($operation) {
612
    $name = rtrim($operation->name, '_');
613
    return "\t['{$name}', '{$operation->signature}', F\\{$operation->name}()],\n";
614
}
615
616
/**
617
 * Generates module's stream methods documentation.
618
 *
619
 * @signature Module -> Module
620
 * @param  array $module
621
 * @return array
622
 */
623
function generate_stream_methods($module) {
624
    $blocks = filter (
625
        satisfiesAll(['ignore' => equals(false), 'stream' => equals(true)]),
626
        $module->blocks
627
    );
628
    $methods = map(stream_method_link($module->name), $blocks);
629
    $module->streamMethods = (length($methods) > 0)
630
        ? "\n\n## {$module->name}\n\n" . join("\n", $methods)
631
        : '';
632
    return $module;
633
}
634
635
/**
636
 * Gets an element of the stream methods list.
637
 *
638
 * @signature String -> Block -> String
639
 * @param  string $moduleName
640
 * @param  object $block
641
 * @return string
642
 */
643
function stream_method_link() {
644
    static $curried = false;
645
    $curried = $curried ?: curry(function($moduleName, $block) {
646
        return "- [{$block->name}](https://github.com/tarsana/functional/blob/master/docs/{$moduleName}.md#{$block->name}) - {$block->summary}\n";
647
    });
648
    return _apply($curried, func_get_args());
649
}
650
651
/**
652
 * process_of(['f1', 'f2']) == pipe(_f('f1'), _f('f2'));
653
 *
654
 * @signature [String] -> Function
655
 * @param array $fns
656
 * @return callable
657
 */
658
function process_of($fns) {
659
    return apply(_f('pipe'), map(_f('_f'), $fns));
660
}
661
662
/**
663
 * Dump a variable and returns it.
664
 *
665
 * @signature a -> a
666
 * @param  mixed $something
667
 * @return mixed
668
 */
669
function log() {
670
    $log = function($something) {
671
        echo toString($something);
672
        return $something;
673
    };
674
    return apply(curry($log), func_get_args());
675
}
676
677
// Convert Warnings to Exceptions
678
set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
0 ignored issues
show
The parameter $errcontext is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
679
    if (0 === error_reporting())
680
        return false;
681
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
682
});
683
684
// Run the build
685
build_main(get_modules());
686