Verified Commit 99487df8 authored by Dave Long's avatar Dave Long
Browse files

Issue #3400458 by alexpott, Berdir, longwave, ReINFaTe, mglaman, larowlan,...

Issue #3400458 by alexpott, Berdir, longwave, ReINFaTe, mglaman, larowlan, Charlie ChX Negyesi, kristiaanvandeneynde, cmlara: AttributeClassDiscovery fails while trying to include non valid plugin class
parent e8d2c22a
Loading
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -79,6 +79,18 @@ public function getUseStatements()
        return $this->staticReflectionParser->getUseStatements();
    }

    /**
     * Determines if the class has the provided class attribute.
     *
     * @param string $attribute The attribute to check for.
     *
     * @return bool
     */
    public function hasClassAttribute(string $attribute)
    {
        return $this->staticReflectionParser->hasClassAttribute($attribute);
    }

    /**
     * {@inheritDoc}
     */
+68 −25
Original line number Diff line number Diff line
@@ -132,6 +132,13 @@ class StaticReflectionParser
     */
    protected $parentStaticReflectionParser;

    /**
     * The class attributes.
     *
     * @var string[]
     */
    protected array $classAttributes = [];

    /**
     * Parses a class residing in a PSR-0 hierarchy.
     *
@@ -178,6 +185,7 @@ protected function parse()
        $tokenParser = new TokenParser($contents);
        $docComment  = '';
        $last_token  = false;
        $attributeNames = [];

        while ($token = $tokenParser->next(false)) {
            switch ($token[0]) {
@@ -187,7 +195,17 @@ protected function parse()
                case T_DOC_COMMENT:
                    $docComment = $token[1];
                    break;
                case T_ATTRIBUTE:
                    while ($token = $tokenParser->next()) {
                        if ($token[0] === T_NAME_FULLY_QUALIFIED || $token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_RELATIVE || $token[0] === T_STRING) {
                            $attributeNames[] = $token[1];
                            break 2;
                        }
                    }
                    break;
                case T_CLASS:
                    // Convert the attributes to fully qualified names.
                    $this->classAttributes = array_map(fn($name) => strtolower($this->fullySpecifyName($name)), $attributeNames);
                    if ($last_token !== T_PAAMAYIM_NEKUDOTAYIM && $last_token !== T_NEW) {
                        $this->docComment['class'] = $docComment;
                        $docComment                = '';
@@ -223,31 +241,7 @@ protected function parse()
                    $docComment                              = '';
                    break;
                case T_EXTENDS:
                    $this->parentClassName = $tokenParser->parseClass();
                    $nsPos                 = strpos($this->parentClassName, '\\');
                    $fullySpecified        = false;
                    if ($nsPos === 0) {
                        $fullySpecified = true;
                    } else {
                        if ($nsPos) {
                            $prefix  = strtolower(substr($this->parentClassName, 0, $nsPos));
                            $postfix = substr($this->parentClassName, $nsPos);
                        } else {
                            $prefix  = strtolower($this->parentClassName);
                            $postfix = '';
                        }
                        foreach ($this->useStatements as $alias => $use) {
                            if ($alias !== $prefix) {
                                continue;
                            }

                            $this->parentClassName = '\\' . $use . $postfix;
                            $fullySpecified        = true;
                        }
                    }
                    if (! $fullySpecified) {
                        $this->parentClassName = '\\' . $this->namespace . '\\' . $this->parentClassName;
                    }
                    $this->parentClassName = $this->fullySpecifyName($tokenParser->parseClass());
                    break;
            }

@@ -341,4 +335,53 @@ public function getStaticReflectionParserForDeclaringClass($type, $name)
        }
        throw new ReflectionException('Invalid ' . $type . ' "' . $name . '"');
    }

    /**
     * Determines if the class has the provided class attribute.
     *
     * @param string $attribute The fully qualified attribute to check for.
     *
     * @return bool
     */
    public function hasClassAttribute(string $attribute): bool
    {
        $this->parse();
        return in_array('\\' . ltrim(strtolower($attribute), '\\'), $this->classAttributes, TRUE);
    }

    /**
     * Converts a name into a fully specified name.
     *
     * @param string $name The name to convert.
     *
     * @return string
     */
    private function fullySpecifyName(string $name): string
    {
        $nsPos          = strpos($name, '\\');
        $fullySpecified = false;
        if ($nsPos === 0) {
            $fullySpecified = true;
        } else {
            if ($nsPos) {
                $prefix  = strtolower(substr($name, 0, $nsPos));
                $postfix = substr($name, $nsPos);
            } else {
                $prefix  = strtolower($name);
                $postfix = '';
            }
            foreach ($this->useStatements as $alias => $use) {
                if ($alias !== $prefix) {
                    continue;
                }

                $name = '\\' . $use . $postfix;
                $fullySpecified        = true;
            }
        }
        if (! $fullySpecified) {
            $name = '\\' . $this->namespace . '\\' . $name;
        }
        return $name;
    }
}
+10 −2
Original line number Diff line number Diff line
@@ -84,16 +84,24 @@ protected function parseClass(string $class, \SplFileInfo $fileinfo): array {
    $finder = MockFileFinder::create($fileinfo->getPathName());
    $parser = new StaticReflectionParser($class, $finder, TRUE);

    $reflection_class = $parser->getReflectionClass();
    // @todo Handle deprecating definitions discovery via annotations in
    // https://www.drupal.org/project/drupal/issues/3265945.
    /** @var \Drupal\Component\Annotation\AnnotationInterface $annotation */
    if ($annotation = $this->getAnnotationReader()->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) {
    if ($annotation = $this->getAnnotationReader()->getClassAnnotation($reflection_class, $this->pluginDefinitionAnnotationName)) {
      $this->prepareAnnotationDefinition($annotation, $class);
      return ['id' => $annotation->getId(), 'content' => $annotation->get()];
    }

    // Annotations use static reflection and are able to analyze a class that
    // extends classes or uses traits that do not exist. Attribute discovery
    // will trigger a fatal error with such classes, so only call it if the
    // class has a class attribute.
    if ($reflection_class->hasClassAttribute($this->pluginDefinitionAttributeName)) {
      return parent::parseClass($class, $fileinfo);
    }
    return ['id' => NULL, 'content' => NULL];
  }

  /**
   * Prepares the annotation definition.
+1 −1
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@
/**
 * Provides a test plugin with a custom attribute.
 */
#[PluginExample(
#[/* comment */PluginExample(
  id: "example_3",
  custom: "George"
)]
+20 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\plugin_test\Plugin\plugin_test\custom_annotation;

use Drupal\plugin_test\Plugin;

/**
 * Provides a test plugin with a custom attribute.
 *
 * This plugin ensures that StaticReflectionParser::parse() correctly determines
 * the fully qualified attribute name.
 *
 * @see \Drupal\Component\Annotation\Doctrine\StaticReflectionParser::parse()
 */
#[Plugin\Attribute\PluginExample(
  id: "example_4",
  custom: "Example 4"
)]
#[\Attribute]
class Example4 {}
Loading