587 lines
17 KiB
Plaintext
587 lines
17 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 = [];
|
|
|
|
/**
|
|
* Other Statements
|
|
*
|
|
* @var array
|
|
* @since 3.2.0
|
|
*/
|
|
protected array $other = [];
|
|
|
|
/**
|
|
* Duplicate Statements
|
|
*
|
|
* @var array
|
|
* @since 3.2.0
|
|
*/
|
|
protected array $duplicate = [];
|
|
|
|
/**
|
|
* 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 = [];
|
|
$this->other = [];
|
|
$this->duplicate = [];
|
|
|
|
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 to determine the necessary class name based on use statements and traits.
|
|
* It checks if the given power (class, trait, etc.) already has a corresponding use statement
|
|
* and handles the naming accordingly to avoid conflicts.
|
|
*
|
|
* @param object $power The power object containing type, namespace, and class name.
|
|
* @param array|null $useStatements Array of existing use statements in the code.
|
|
* @param array|null $traits Array of existing traits used in the code.
|
|
*
|
|
* @return string|null The determined class name, or null if the type is not valid.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function inspect(object $power, ?array $useStatements, ?array $traits): ?string
|
|
{
|
|
$namespaceStatement = $this->buildNamespaceStatment($power);
|
|
|
|
$use_extracted = $this->extractUseStatements($namespaceStatement, $power->class_name, $useStatements);
|
|
|
|
$name = $use_extracted['found'] ?? $power->class_name;
|
|
|
|
$name = $this->getUniqueName($name, $power);
|
|
|
|
$this->handleTraitLogic($name, $power, $traits);
|
|
|
|
if (!$use_extracted['hasStatement'])
|
|
{
|
|
$this->addUseStatement($name, $power->class_name, $namespaceStatement);
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Builds the namespace statement from the power object's namespace and class name.
|
|
*
|
|
* @param object $power The power object.
|
|
*
|
|
* @return string The constructed use statement.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function buildNamespaceStatment(object $power): string
|
|
{
|
|
return $power->_namespace . '\\' . $power->class_name;
|
|
}
|
|
|
|
/**
|
|
* Extracts and processes use statements to find if the current class name is already used.
|
|
* It identifies any potential naming conflicts.
|
|
*
|
|
* @param string $useStatement The search statement of the current class.
|
|
* @param string $className The class name of the power object.
|
|
* @param array|null $useStatements The existing use statements.
|
|
*
|
|
* @return array An array with keys 'found' and 'hasStatement'.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function extractUseStatements(string $useStatement, string $className, ?array $useStatements): array
|
|
{
|
|
$results = ['found' => null, 'hasStatement' => false];
|
|
|
|
if ($useStatements !== null)
|
|
{
|
|
foreach ($useStatements as $use_statement)
|
|
{
|
|
$class_name = $this->extractClassNameOrAlias($use_statement);
|
|
|
|
if ($this->isUseStatementEqual($use_statement, $useStatement))
|
|
{
|
|
if ($results['found'] === null)
|
|
{
|
|
$results['found'] = $class_name;
|
|
$results['hasStatement'] = true;
|
|
}
|
|
else
|
|
{
|
|
// TODO we need to backport fix these
|
|
$this->duplicate[$use_statement] = $class_name;
|
|
}
|
|
}
|
|
elseif ($className === $class_name)
|
|
{
|
|
$this->other[$className] = $class_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Checks if the namespace statement is already declared in the current use statements.
|
|
*
|
|
* This method uses a regular expression to check for an exact match of the full statement,
|
|
* taking into account the possibility of an alias being used.
|
|
*
|
|
* @param string $useStatement The existing use statement to check against.
|
|
* @param string $namespaceStatement The search statement to search for (without the trailing semicolon, or use prefix).
|
|
*
|
|
* @return bool True if the full statement is found, false otherwise.
|
|
*/
|
|
protected function isUseStatementEqual(string $useStatement, string $namespaceStatement): bool
|
|
{
|
|
// Create a regular expression pattern to match the full statement
|
|
// The pattern checks for the start of the statement, optional whitespace,
|
|
// and an optional alias after the full statement.
|
|
$pattern = '/^use\s+' . preg_quote($namespaceStatement, '/') . '(?:\s+as\s+\w+)?;$/';
|
|
|
|
// Perform the regex match to check if the use statement is equal to the search statment
|
|
return (bool) preg_match($pattern, $useStatement);
|
|
}
|
|
|
|
/**
|
|
* Extracts the class name or alias from a use statement.
|
|
*
|
|
* This method parses a PHP 'use' statement and extracts either the class name or its alias.
|
|
* If the statement doesn't match the expected format, or if no class name or alias is found,
|
|
* the method returns null.
|
|
*
|
|
* Example:
|
|
* - 'use Namespace\ClassName;' -> returns 'ClassName'
|
|
* - 'use Namespace\ClassName as Alias;' -> returns 'Alias'
|
|
*
|
|
* @param string $useStatement The use statement from which to extract the class name or alias.
|
|
*
|
|
* @return string|null The class name or alias if found, null otherwise.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function extractClassNameOrAlias(string $useStatement): ?string
|
|
{
|
|
// If the input doesn't start with 'use ', assume it's just the namespace without a use statement
|
|
if (strpos($useStatement, 'use ') !== 0)
|
|
{
|
|
return $this->extractLastNameFromNamespace($useStatement);
|
|
}
|
|
|
|
// Regular expression to extract the class name and alias from the use statement
|
|
$pattern = '/use\s+(?P<namespace>[\w\\\\]+?)(?:\s+as\s+(?P<alias>\w+))?;/';
|
|
|
|
if (preg_match($pattern, $useStatement, $matches))
|
|
{
|
|
// Return the alias if it exists; otherwise, return the last part of the namespace (class name)
|
|
return $matches['alias'] ?? $this->extractLastNameFromNamespace($matches['namespace']);
|
|
}
|
|
|
|
// Return null if no match is found
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ensures the name for the use statement is unique, avoiding conflicts with other classes.
|
|
*
|
|
* @param string $name The current name
|
|
* @param object $power The power object containing type, namespace, and class name.
|
|
*
|
|
* @return string The unique name
|
|
* @since 3.2.0
|
|
*/
|
|
protected function getUniqueName(string $name, object $power): string
|
|
{
|
|
// set search namespace
|
|
$namespace = ($name !== $power->class_name) ? $this->buildNamespaceStatment($power) : $power->_namespace;
|
|
|
|
// check if we need to update the name
|
|
if (isset($this->other[$name]))
|
|
{
|
|
// if the name is already used
|
|
while (isset($this->other[$name]))
|
|
{
|
|
if (($tmp = $this->extractClassNameOrAlias($namespace)) !== null)
|
|
{
|
|
$name = ucfirst($tmp) . $name;
|
|
$namespace = $this->removeLastNameFromNamespace($namespace);
|
|
}
|
|
else
|
|
{
|
|
$name = 'Unique' . $name;
|
|
}
|
|
}
|
|
}
|
|
|
|
// also loop new found use statements
|
|
if (isset($this->useStatements[$name]))
|
|
{
|
|
// if the name is already used
|
|
while (isset($this->useStatements[$name]))
|
|
{
|
|
if (($tmp = $this->extractClassNameOrAlias($namespace)) !== null)
|
|
{
|
|
$name = ucfirst($tmp) . $name;
|
|
$namespace = $this->removeLastNameFromNamespace($namespace);
|
|
}
|
|
else
|
|
{
|
|
$name = 'Unique' . $name;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* Extracts the last part of a namespace string, which is typically the class name.
|
|
*
|
|
* @param string $namespace The namespace string to extract from.
|
|
*
|
|
* @return string|null The extracted class name.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function extractLastNameFromNamespace(string $namespace): ?string
|
|
{
|
|
$parts = explode('\\', $namespace);
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Removes the last name from the namespace.
|
|
*
|
|
* @param string $namespace The namespace
|
|
*
|
|
* @return string The namespace shortened
|
|
* @since 3.2.0
|
|
*/
|
|
protected function removeLastNameFromNamespace(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);
|
|
}
|
|
|
|
/**
|
|
* Determines whether a trait statement should be added.
|
|
*
|
|
* @param object $power The power object.
|
|
*
|
|
* @return bool True if a trait statement should be added, false otherwise.
|
|
* @since 3.2.0
|
|
*/
|
|
protected function shouldAddTraitStatement(object $power): bool
|
|
{
|
|
return $power->type === 'trait';
|
|
}
|
|
|
|
/**
|
|
* Handles specific logic for traits, such as checking if the trait is already used.
|
|
*
|
|
* @param string $name The current name.
|
|
* @param object $power The power object containing type, namespace, and class name.
|
|
* @param array|null $traits The traits used in the code.
|
|
*
|
|
* @return void
|
|
* @since 3.2.0
|
|
*/
|
|
protected function handleTraitLogic(string $name, object $power, ?array $traits): void
|
|
{
|
|
if ($this->shouldAddTraitStatement($power) && $traits !== null)
|
|
{
|
|
foreach ($traits as $trait)
|
|
{
|
|
if ($trait === $name)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// add the trait
|
|
$this->traits[$name] = 'use ' . $name . ';';
|
|
}
|
|
|
|
/**
|
|
* Adds a use statement to the class if it's not already present.
|
|
*
|
|
* @param string $name The name to use.
|
|
* @param string $className The class name of the power object.
|
|
* @param string $namespaceStatement The search statement to search for (without the trailing semicolon, or use prefix).
|
|
*
|
|
* @since 3.2.0
|
|
*/
|
|
protected function addUseStatement(string &$name, string $className, string $namespaceStatement): void
|
|
{
|
|
if ($name !== $className)
|
|
{
|
|
$statement = 'use ' . $namespaceStatement . ' as ' . $name . ';';
|
|
}
|
|
else
|
|
{
|
|
$statement = 'use ' . $namespaceStatement . ';';
|
|
}
|
|
|
|
$this->useStatements[$name] = $statement;
|
|
}
|
|
|
|
/**
|
|
* 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, $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, $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\('_JEXEC'\)(.*?)\s*;/",
|
|
"/defined\('JPATH_BASE'\)(.*?)\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;
|
|
} |