jcb-compiler/src/763d137c-42bc-4282-98d8-cc5.../code.power

452 lines
12 KiB
Plaintext

/**
* Power Objects
*
* @var Power
* @since 3.2.0
**/
protected Power $power;
/**
* Compiler Powers Extractor
*
* @var Extractor
* @since 3.2.0
**/
protected Extractor $extractor;
/**
* Compiler Powers Parser
*
* @var Parser
* @since 3.2.0
**/
protected Parser $parser;
/**
* Compiler Placeholder
*
* @var Placeholder
* @since 3.2.0
*/
protected Placeholder $placeholder;
/**
* Super Power Update Map
*
* @var array
* @since 3.2.0
*/
protected array $map = [];
/**
* Insert Use Statements
*
* @var array
* @since 3.2.0
*/
protected array $useStatements = [];
/**
* Insert Trait Statements
*
* @var array
* @since 3.2.0
*/
protected array $traits = [];
/**
* Constructor.
*
* @param Power|null $power The power object.
* @param Extractor|null $extractor The powers extractor object.
* @param Parser|null $parser The powers parser object.
* @param Placeholder|null $placeholder The compiler placeholder object.
*
* @since 3.2.0
*/
public function __construct(?Power $power = null, ?Extractor $extractor = null,
?Parser $parser = null, ?Placeholder $placeholder = null)
{
$this->power = $power ?: Compiler::_('Power');
$this->extractor = $extractor ?: Compiler::_('Power.Extractor');
$this->parser = $parser ?: Compiler::_('Power.Parser');
$this->placeholder = $placeholder ?: Compiler::_('Placeholder');
}
/**
* Inject the powers found in the code
*
* @param string $code The class code
*
* @return string The updated code
* @since 3.2.0
*/
public function power(string $code): string
{
if (($guids = $this->extractor->get($code)) !== null)
{
return $this->update($guids, $code);
}
return $code;
}
/**
* Update the code
*
* @param array $guids The Power guids found
* @param string $code The class code
*
* @return string The updated code
* @since 3.2.0
*/
protected function update(array $guids, string $code): string
{
$use_statements = $this->parser->getUseStatements($code);
$traits = $this->parser->getTraits(
$this->parser->getClassCode($code) ?? ''
);
// reset with each file
$this->map = [];
$this->useStatements = [];
$this->traits = [];
foreach ($guids as $key => $guid)
{
if (($power = $this->power->get($guid)) !== null)
{
if (($name = $this->inspect($power, $use_statements, $traits)) !== null)
{
$this->map[$key] = $name;
}
}
}
// update
if ($this->map !== [])
{
if ($this->useStatements !== [])
{
$code = $this->addUseStatements($code, $use_statements);
}
return $this->placeholder->update($code, $this->map);
}
return $code;
}
/**
* inspect the super power
*
* @param object|null $power The power object.
* @param array|null $useStatements The code use statments
* @param array|null $traits The code traits use statments
*
* @return string|null The class name (or as name)
* @since 3.2.0
*/
protected function inspect(object $power, ?array $useStatements, ?array $traits): ?string
{
if (isset($power->type) && in_array($power->type, ['class', 'abstract class', 'final class', 'trait']))
{
$statement = 'use ' . $power->_namespace . '\\' . $power->class_name;
// other class names
$use_other = [];
$trait_other = [];
// some tracker globals
$has_use_statement = false; // add if not found
$has_trait_statement = !('trait' === $power->type); // don't add if not trait
$name = null;
$trait_name = null;
// check if the name space is loaded
if ($useStatements !== null)
{
foreach ($useStatements as $use_statement)
{
if ($use_statement === $statement . ';' || strpos($use_statement, $statement . ' as ') !== false)
{
$name = $this->getName($use_statement);
$has_use_statement = true;
}
else
{
$tmp = $this->getName($use_statement);
if ($power->class_name === $tmp)
{
$use_other[$tmp] = $tmp;
}
}
}
}
// check if the trait is loaded
if (!$has_trait_statement && $traits !== null)
{
$trait_statement = $name ?? $power->class_name;
foreach ($traits as $trait)
{
if ($trait === $trait_statement)
{
$trait_name = $trait;
$has_trait_statement = true;
}
}
}
// build the name
$name = $trait_name ?? $name ?? $power->class_name;
// if we have a trait we may need to add use and trait
if ('trait' === $power->type)
{
if (!$has_trait_statement)
{
$this->traits[$name] = 'use ' . $name . ';';
}
}
// check if we need to update the name
if ($use_other !== [])
{
// set search namespace
$namespace = ($name !== $power->class_name) ? $power->_namespace . '\\' . $power->class_name : $power->_namespace;
// get the unique name
$name = $this->getUniqueName($name, $namespace, $use_other);
}
if (!$has_use_statement)
{
// if the name is not the same as class name
if ($name !== $power->class_name)
{
$statement .= ' as ' . $name . ';';
}
else
{
$statement .= ';';
}
$this->useStatements[$name] = $statement;
}
return $name;
}
return null;
}
/**
* Extracts the class name from a use statement.
*
* @param string $useStatement The use statement from which to extract the class name
*
* @return string|null The class name or null if not found
* @since 3.2.0
*/
protected function getName(string $useStatement): ?string
{
// If the input doesn't start with 'use ', assume it's a class name without a use statement
if (strpos($useStatement, 'use ') !== 0)
{
$parts = explode('\\', $useStatement);
$result = end($parts);
// Remove '\\' from the beginning and end of the resulting string
$result = trim($result, '\\');
// If the resulting string is empty, return null
return empty($result) ? null : $result;
}
$pattern = '/use\s+([\w\\\\]+)(?:\s+as\s+)?([\w]+)?;/';
if (preg_match($pattern, $useStatement, $matches))
{
// If there's an alias, return it
if (!empty($matches[2]))
{
return $matches[2];
}
// If there's no alias, extract the class name from the namespace
$parts = explode('\\', $matches[1]);
return end($parts);
}
return null;
}
/**
* Removes the last space from the namespace.
*
* @param string $name The current name
* @param string $namespace The namespace
* @param array $useOther The other use names
*
* @return string The namespace shortened
* @since 3.2.0
*/
protected function getUniqueName(string $name, string $namespace, array $useOther): string
{
// if the name is already used
while (isset($useOther[$name]))
{
if (($tmp = $this->getName($namespace)) !== null)
{
$name = ucfirst($tmp) . $name;
$namespace = $this->removeLastSpace($namespace);
}
}
return $name;
}
/**
* Removes the last space from the namespace.
*
* @param string $namespace The namespace
*
* @return string The namespace shortened
* @since 3.2.0
*/
protected function removeLastSpace(string $namespace): string
{
// Remove '\\' from the beginning and end of the resulting string
$namespace = trim($namespace, '\\');
$parts = explode('\\', $namespace);
// Remove the last part (the class name)
array_pop($parts);
// Reassemble the namespace without the class name
return implode('\\', $parts);
}
/**
* Insert a line before the class declaration in the given class code.
*
* @param string $code The class code
* @param array|null $useStatements The existing use statements
*
* @return string The modified file content
* @since 3.2.0
*/
protected function addUseStatements(string $code, ?array $useStatements): string
{
if ($useStatements !== null)
{
// we add the use statements using existing use statements
$key = end($useStatements);
array_unshift($this->useStatements, $key);
return $this->placeholder->update($code, [$key => implode(PHP_EOL, array_values($this->useStatements))]);
}
return $this->addLines($code, implode(PHP_EOL, array_values($this->useStatements)));
}
/**
* Insert a line before the class declaration in the given class code.
*
* @param string $code The class code
* @param string $lines The new lines to insert
*
* @return string The modified file content
* @since 3.2.0
*/
protected function addLines(string $code, string $lines): string
{
// Pattern to match class, final class, abstract class, interface, and trait
$pattern = '/(?:class|final class|abstract class|interface|trait)\s+[a-zA-Z0-9_]+\s*(?:extends\s+[a-zA-Z0-9_]+\s*)?(?:implements\s+[a-zA-Z0-9_]+(?:\s*,\s*[a-zA-Z0-9_]+)*)?\s*\{/s';
// Find the position of the class declaration
preg_match($pattern, $code, $matches, PREG_OFFSET_CAPTURE);
$class_declaration_pos = $matches[0][1] ?? null;
if (null !== $class_declaration_pos)
{
// Find the position of the last newline character before the class declaration
$last_newline_pos = strrpos($code, PHP_EOL, -(strlen($code) - $class_declaration_pos));
// Find the position of the comment block right before the class declaration
$comment_pattern = '/\s*\*\/\s*$/m';
$insert_pos = null;
if (preg_match($comment_pattern, $code, $comment_matches, PREG_OFFSET_CAPTURE, 0, $last_newline_pos))
{
$insert_pos = (int) $comment_matches[0][1] + strlen($comment_matches[0][0]);
}
else
{
// Find the last empty line before the class declaration
$empty_line_pattern = '/(^|\r\n|\r|\n)[\s]*($|\r\n|\r|\n)/';
if (preg_match($empty_line_pattern, $code, $empty_line_matches, PREG_OFFSET_CAPTURE, 0, $last_newline_pos))
{
$insert_pos = (int) $empty_line_matches[0][1] + strlen($empty_line_matches[0][0]);
}
}
// Insert the new line at the found position
if (null !== $insert_pos)
{
return substr_replace($code, $lines . PHP_EOL, $insert_pos, 0);
}
}
// last try targeting the defined line
return $this->addLinesAfterDefinedLine($code, $lines);
}
/**
* Inserts a new line after the defined('_JEXEC') line.
*
* @param string $code The class code
* @param string $lines The new lines to insert
*
* @return string The modified file content
* @since 3.2.0
*/
protected function addLinesAfterDefinedLine(string $code, string $lines): string
{
// Patterns to match the defined('_JEXEC') and defined('JPATH_BASE') lines
$patterns = [
"/defined\('_JEXEC'\)(.*?)\s*;/",
"/defined\('JPATH_BASE'\)(.*?)\s*;/",
];
$insert_pos = null;
// Iterate through the patterns and try to find a match
foreach ($patterns as $pattern)
{
preg_match($pattern, $code, $matches, PREG_OFFSET_CAPTURE);
$defined_line_pos = $matches[0][1] ?? null;
if ($defined_line_pos !== null)
{
// Find the position of the newline character after the defined() line
$next_lines_pos = strpos($code, PHP_EOL, (int) $defined_line_pos + strlen($matches[0][0]));
// Insert the new line at the found position
if ($next_lines_pos !== false)
{
$insert_pos = $next_lines_pos;
break;
}
}
}
// Insert the new line at the found position
if ($insert_pos !== null)
{
$code = substr_replace($code, PHP_EOL . $lines, $insert_pos, 0);
}
return $code;
}