<?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;
}
}