Skip to content

feat(drupal): extract service-container wiring and plugin definitions#1077

Open
mikedamoiseau wants to merge 1 commit into
colbymchenry:mainfrom
mikedamoiseau:feat/drupal-services-and-plugins
Open

feat(drupal): extract service-container wiring and plugin definitions#1077
mikedamoiseau wants to merge 1 commit into
colbymchenry:mainfrom
mikedamoiseau:feat/drupal-services-and-plugins

Conversation

@mikedamoiseau

Copy link
Copy Markdown

What

Fills the first two open TODOs in the Drupal framework resolver
(src/resolution/frameworks/drupal.ts): service-container wiring and
plugin definitions. Both were explicitly listed as "TODOs for future
iterations" in the resolver's header docblock and are now implemented,
extending the existing composer/.info.yml detection, *.routing.yml
route resolution, and hook detection.

Supports Drupal 8/9/10/11. No new dependencies; hand-rolled line-based parsing
mirroring the existing extractDrupalRoutes() shape.

Nodes & edges now emitted

Drupal has no dedicated service/plugin NodeKind, so both reuse the
generic component kind (as routes use route), staying distinguishable by
qualifiedName prefix.

1. Service definitions — *.services.yml

services:
  my_module.foo:
    class: Drupal\my_module\Foo
    arguments: ['@other.service', '@database']
  • a component node per service id — qualifiedName = …::service:<id>
  • references edge: service → its class: FQCN (resolved to the class node)
  • references edges: service → each @service argument (service → service,
    wiring the DI graph) — handles both the inline ['@a', '@b'] form and the
    multi-line block-list form
  • DI directive keys (_defaults, _instanceof) are skipped; trailing comments
    are stripped; argument resolution is scoped to refs originating in a
    *.services.yml file so it can't hijack a routing.yml entity-handler ref
    that happens to share a dotted name.

This makes Drupal's dependency injection — previously invisible — traceable via
codegraph_explore.

2. Plugin definitions — PHP

Drupal 11 is attribute-first, but a large body of contrib/legacy code still uses
docblock annotations, so both are parsed:

#[Block(id: 'shortcuts', admin_label: new TranslatableMarkup("Shortcuts"))]
class ShortcutsBlock extends BlockBase {}
/**
 * @Block(
 *   id = "weller_hero",
 *   admin_label = @Translation("Weller Hero"),
 * )
 */
class Hero extends BlockBase {}
  • a component node per plugin id — qualifiedName = …::plugin:<Type>:<id>
  • references edge: plugin → the decorated class
  • a balanced-paren body scan captures the id even when it follows a nested
    @Translation(...) (common in core, e.g. label before id)
  • a bounded forward class search keeps a stray plugin-shaped token (in a string
    or comment) from binding to an unrelated later class
  • only known plugin types are matched (see DRUPAL_PLUGIN_TYPES), so unrelated
    attributes like PHPUnit's #[DataProvider] / #[Group] are ignored.

Deliberately left out

  • Plugin types not in the known setDRUPAL_PLUGIN_TYPES is the common
    core+contrib set; an unlisted custom plugin type is ignored (silent beats
    wrong / false plugins). Easy to extend.
  • Only the plugin id is captured — other attribute/annotation arguments
    (admin_label, label, default_widget, …) are intentionally not extracted.
  • Class-FQCN-as-service-id shorthand (Drupal\My\Service: ~) — a rare
    Symfony form; documented as a known gap.
  • Twig — unchanged (still no tree-sitter Twig grammar).

Tests

28 new cases in __tests__/drupal.test.ts, TDD (failing first):

  • services.yml extract: node per id, qualifiedName shape, service→class
    edge, service→service @argument edges, ignores the top-level services
    key, empty file.
  • services.yml edge cases (from a thorough code-review pass): multi-line
    block-list arguments, _defaults/_instanceof skipped, trailing comment on
    the id line, no @token capture from a trailing comment.
  • service resolve(): bare class FQCN, dotted and dotless service-arg ids,
    and a regression test that a routing.yml entity-handler ref is not
    mis-resolved to a same-named service.
  • plugin attributes: multi-line #[Block], single-line single-quote,
    #[FieldType], ignores #[DataProvider].
  • plugin annotations: @Block, @FieldType, id after a nested
    @Translation(...), nested annotation not treated as a plugin, no binding of
    a stray token to a far-away class.

Validation

npm test green (Drupal suite 53/53; full suite unaffected). Validated against
a real Drupal 11 codebase (~23 custom modules): 53 services, 27 plugins
extracted; all 27 plugin→class edges and all in-index service→class edges
resolve; service→service DI edges resolve for services present in the index
(core-service args correctly stay unresolved in a modules-only index); node
count stable; 0 bogus _defaults/_instanceof nodes.

The TODOs these address are an open maintainer invitation in the resolver
docblock — the header has been updated to reflect what's now implemented and
the remaining Twig TODO.

Fills the two open TODOs in the Drupal framework resolver.

Services (*.services.yml):
- emit a component node per service id (qualifiedName service:<id>)
- references edge service -> its class: FQCN
- references edges service -> each @service argument (inline AND block-list
  forms), so codegraph_explore can trace Drupal's DI container
- skip DI directive keys (_defaults/_instanceof); strip trailing comments;
  argument resolution is scoped to refs originating in *.services.yml so it
  cannot hijack a routing.yml entity-handler ref sharing a dotted name

Plugins (PHP):
- emit a component node per plugin id (qualifiedName plugin:<Type>:<id>)
- references edge plugin -> the decorated class
- handle BOTH Drupal 11 PHP 8 attributes (#[Block(id: 'foo')], #[FieldType...])
  and legacy docblock annotations (@block(id = "foo"), @FieldType...)
- balanced-paren body scan so an id following a nested @translation(...) is
  still captured; bounded forward class search so a stray plugin-shaped token
  does not bind to an unrelated later class

Validated on a real Drupal 11 codebase (53 services, 27 plugins): all plugin
and in-index service class edges resolve; no node-count explosion.

Tests: 28 new cases in __tests__/drupal.test.ts (services.yml extract/resolve,
plugin attributes + annotations, and code-review regression cases).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant