PhpGrammar.php

<?php

namespace Tlf\Lexer\Test;


class PhpGrammar extends Tester {

    use Directives\Props;
    use Directives\Vars;
    use Directives\Args;
    use Directives\Methods;
    use Directives\Classes;
    use Directives\ClassIntegration;
    use Directives\Traits;
    use Directives\Namespaces;
    use Directives\UseTrait;
    use Directives\PhpOpenTags;
    use Directives\Consts;
    use Directives\Values;
    use Directives\Other;

    protected $directive_tests;

    public function prepare(){
        $this->directive_tests = array_merge(
            $this->_prop_tests, 
            $this->_arg_tests,
            $this->_namespace_tests,
            $this->_method_tests,
            $this->_class_tests,
            $this->_class_integration_tests,
            $this->_trait_tests,
            $this->_php_open_tags_tests,
            $this->_const_tests,
            $this->_use_trait_tests,
            $this->_values_tests,
            $this->_other_tests,
            $this->_var_tests,
        );
    }

    public function input_file($file){
        return $this->file('test/input/php/lex/').$file.'.php';
    }

    /**
     * Unit test individual directives from the `test/src/Php/*.php` files
     *
     * For a summary of the directives, run `phptest -test testShowMePhpFeatures`, then see `test/output/PhpFeatures.md`
     */
    public function testDirectives(){
        // i removed body from the ast ...
        // so this is probably making many fail

        $phpGram = new \Tlf\Lexer\PhpGrammar();
        $docblockGram = new \Tlf\Lexer\DocblockGrammar();
        $grammars = [
            $phpGram,
            $docblockGram,
        ];
        $phpGram->buildDirectives();


        $this->runDirectiveTests($grammars, $this->directive_tests);
    }


    public function testLilMigrationsBug(){
        // may 13, 2022
        // There is a bug with properties when there is an anonymous function with a `use` statement and there is a body to the function.
        // removing the use() statement OR removing the function body "fixes" it
        $this->assert_file('lildb/LilMigrationsBug', false, -1);
        // my solve was to capture the use statement, with the help of existing method_arglist instructions
    }

    public function testLilMigrations(){
        // see testLilMigrationsBug for more information why this is here.
        $this->assert_file('lildb/LilMigrations', false, -1);
    }

    public function testLilDb(){
        $this->assert_file('lildb/LilDb',false,-1);
    }
    public function testPhadFormsTest(){
        $this->assert_file('phad/FormsTest',false,);
    }

    public function testScrawlFnTemplate(){
        $this->assert_file('code-scrawl/functionListTemplate',false,);
    }

    /**
     * @todo clean this up and separate tests as needed. 
     * @todo fix counts that are failing
     */
    public function testMethodParseErrors(){
        $this->assert_file('MethodParseErrors',false);
    }

    public function testPhtmlNode(){
        // echo "The failure is comments. There are 31 comments, but its only finding 3 because body is not yet implemented";
        $this->assert_file('phtml/Node',false);
    }

    public function testPhtmlParser(){
        $this->assert_file('phtml/PHPParser',false);
    }

    public function testPhtml(){
        $this->assert_file('phtml/Phtml',false);
    }

    public function testPhtmlCompiler(){
        $this->assert_file('phtml/Compiler',false);
    }
    public function testPhtmlTextNode(){
        $this->assert_file('phtml/TextNode',false);
    }

    public function testSampleClass(){
        $this->assert_file('SampleClass', false);
    }


    /**
     * Get an ast tree from a file
     * @see assert_file()
     */
    public function parse_file($file, $debug, $stop_loop){
        $file = $this->input_file($file);
        $input = file_get_contents($file);


        $phpGram = new \Tlf\Lexer\PhpGrammar();
        $phpGram->directives = array_merge(
            $phpGram->_string_directives,
            $phpGram->_core_directives,
        );
        $lexer = new \Tlf\Lexer();
        $lexer->stop_loop = $stop_loop;
        $lexer->debug = $debug;
        $lexer->addGrammar($phpGram, null, false);
        $lexer->addDirective($phpGram->getDirectives(':php_open')['php_open']);

        $ast = new \Tlf\Lexer\Ast('file');
        $ast = $lexer->lex($input, $ast);

        $tree = $ast->getTree();

        return $tree;
    }
    /**
     * @param $file the file inside test/output/php/tree
     * @param $tree the ast to write to disk
     */
    public function output_tree(string $file, array $tree){
        $out = $this->file('test/output/php/tree/').$file;
        $json = json_encode($tree, JSON_PRETTY_PRINT);
        $tree_print = print_r($tree,true);

        $dir = dirname($out);
        if (!is_dir($dir))mkdir($dir, 0754, true);

        // i like the highlighting better when i make it .js
        file_put_contents($out.'.printr.js', $tree_print);
        file_put_contents($out.'.js', $json);
    }

    /**
     * get the expected counts from `test/php/counts/$file.json`
     */
    public function get_counts($file){
        $expect_file = $this->file('test/input/php/counts/').$file.'.json';
        if (!file_exists($expect_file))return;
        $expect = json_decode(file_get_contents($expect_file),true);
        return $expect;
    }

    /**
     * Assert that the ast tree contains the given counts of items
     */
    public function assert_counts(array $ast_tree, array $target_counts){
        $actual_counts = $this->get_tree_counts($ast_tree, []);
        foreach ($target_counts as $key=>$count){
            $this->test($key.' counts');
            $this->compare($count, $actual_counts[$key]??0);
        }

        echo "\n\nTarget Counts:\n";
        print_r($target_counts);
        echo "\nActual Counts:\n";
        print_r($actual_counts);
    }

    /**
     *
     * Run tests on the given file. (currently just tree counts)
     *
     * 1. Parses the input file into an ast tree
     * 2. Loads InputFile.expect.js, which contains things like the expected number of consts, methods, and comments
     * 3. Compares the output ast against the expected counts
     * 4. Writes the ast tree to `test/input/php/tree/{$file}.js` & `.../{$file.printr.js}`
     *    - this is just for visual verification (i think)
     *    - this should be changed to the output dir
     * 
     * @param $file a relative path to a file in `test/input/php/lex/`
     * @param $debug true/false to enable debugging
     * @param $stop_loop -1 never to stop or an integer to stop at the given loop
     *
     */
    public function assert_file($file, bool $debug=true, int $stop_loop = -1){
        echo "\nParse $file\n";

        $tree = $this->parse_file($file,$debug,$stop_loop);

        $this->output_tree($file, $tree);

        $counts = $this->get_counts($file);

        unset($counts['--comment']);
        $this->assert_counts($tree, $counts);

    }



    /**
     * This is not really a test. 
     * 
     * It writes file `test/output/PhpFeatures.md` showing a synopsis of which directives passed / failed & what their input was
     *
     * The output is like:
     *  -FailedTestname: input string that was lexed
     *  +PassedTestName: input string that was lexed
     */
    public function testShowMePhpFeatures(){
        $this->disable();

        ob_start();
        $phpGram = new \Tlf\Lexer\PhpGrammar();
        $docblockGram = new \Tlf\Lexer\DocblockGrammar();
        $grammars = [
            $phpGram,
            $docblockGram,
        ];
        $phpGram->buildDirectives();

        $test_results = $this->runDirectiveTests($grammars, $this->directive_tests);
        ob_end_clean();

        $fh = fopen($this->file('test/output/PhpFeatures.md'),'w');
        foreach ($this->directive_tests as $name=>$test){
            $status = 'fail';
            $s = '-';
            if ($test_results[$name]){
                $s = '+';
                $status = 'pass';
            }
            $str = "\n$s$name: ".$test['input'];
            echo $str;
            $str = "\n- $s$name($status): `".$test['input'].'`';
            fwrite($fh, $str);
        }
        fclose($fh);
    }


    /**
     * test an individual file (or all files in a directory) as needed
     *
     * @usage `phptest -test RunFile -file NameOfFileOrDir` where the file (or dir) must be within `test/input/php/lex/`
     */
    public function testRunFile(){
        $file = $this->options['file'] ?? '';
        if ($file==''){
            echo "To use this: phptest -test RunFile -file NameOfFileOrDir";
            $this->disable();
            return;
        }
        $path = $this->input_file($filek);
        if (is_file($path)){
            // echo 'zeep';
            // exit;
            $this->assert_file(substr($file,0,-4), false);
        } else {
            foreach (scandir($path) as $sub_file){
                if (substr($sub_file,-4)!='.php')continue;
                var_dump($file.'/'.$sub_file);
                exit;
                $this->assert_file(substr($file.'/'.$sub_file));
            }
        }


        return;
        if (isset($this->options['file'])){
            $file = $this->options['file'];
            var_dump($file);
            exit;
        }
    }


    /**
     * Test the test function
     */
    public function testGetTreeCounts(){
        $counts = [
            'class'=>3,
            'methods'=>3,
            'name'=>2,
            'namespace'=>1,
            'declaration'=>3,
            'type'=>4,
        ];
        $tree = [
            'namespace'=>[
                'type'=>'namespace',
                'class'=>[
                    0=>[
                        'name'=>'SomeClass',
                        'methods'=>[
                            0=>[
                                'type'=>'method',
                                'declaration'=>'function(){}',
                            ],
                            1=>[
                                'type'=>'method',
                                'declaration'=>'function(){}',
                            ],
                        ]
                    ],
                    1=>[
                        'name'=>'SomeClass2',
                        'methods'=>[
                            0=>[
                                'type'=>'method',
                                'declaration'=>'function(){}',
                            ],
                        ]
                    ],
                ],
            ],
        ];
        $final = $this->get_tree_counts($tree, []);
        print_r($final);
        $this->compare(
            $final,
            [
                'namespace'=>1,
                'type'=>4,
                'class'=>2,
                'name'=>2,
                'methods'=>3,
                'declaration'=>3
            ],
        );
    }

    /**
     * get an array of counts across an entire array.
     * Each key increases by one when it is found,
     * except when the value is an array containing numeric indices,
     * then the count[key] increases by count(value)
     * @return the counts
     */
    public function get_tree_counts($array, $counts){
        
        // every time i encounter a string key:
        // if the value is an array with numeric indices,
            // increase the count[key] by count(array value)
            // visit each child
        // if the value is an array with string keys, 
            // increase count[key] by 1
            // visit each child
        // if the value is not an array
            // increase count[key] by 1
        foreach ($array as $key=>$value){
            if (!isset($counts[$key]))$counts[$key] = 0;
            if (is_array($value)&&$this->has_numeric_indices($value)){
                $counts[$key] += count($value);
                $counts = $this->get_tree_counts($value, $counts);
            } else if (is_array($value)){
                $counts[$key] += 1;
                $counts = $this->get_tree_counts($value, $counts);
            } else {
                $counts[$key] += 1;
            }
            if (is_int($key))unset($counts[$key]);
        }

        return $counts;
    }

    /**
     * @return true if all the array's keys are numeric, false otherwise
     */
    public function has_numeric_indices(array $array): bool{
        $keys = array_keys($array);
        $keys = implode('',$keys);
        if (is_numeric($keys))return true;
        return false;
    }
}