@@ -539,4 +539,270 @@ When the callee has multiple statements:
539539| Feature | What it contributes |
540540| ---| ---|
541541| Go-to-Definition | Resolves call site to the callee's definition location and source |
542- | Extract Function (§3) scope analysis | Variable collision detection at the call site |
542+ | Extract Function (§3) scope analysis | Variable collision detection at the call site |
543+
544+ ---
545+
546+ ## 10. Extract Constant
547+ ** Impact: Medium · Effort: Medium**
548+
549+ Select a literal value (string, integer, float, boolean) inside a class
550+ and extract it into a class constant. This pairs naturally with Extract
551+ Variable (§8) and shares the same "select, name, replace" workflow.
552+
553+ ### Behaviour
554+
555+ - ** Trigger:** The user selects a literal expression inside a class
556+ method or property default. The code action introduces a new class
557+ constant with a generated name, assigns the literal value, and
558+ replaces the selection (and optionally all identical occurrences in
559+ the class) with ` self::CONSTANT_NAME ` .
560+ - ** Code action kind:** ` refactor.extract ` .
561+
562+ ### What can be extracted
563+
564+ - String literals: ` 'pending' ` , ` "active" ` .
565+ - Integer literals: ` 200 ` , ` 0xFF ` .
566+ - Float literals: ` 3.14 ` .
567+ - Boolean literals: ` true ` , ` false ` (less common but valid).
568+ - Concatenated string expressions: ` 'prefix_' . 'suffix' ` — extract the
569+ whole expression as a single constant.
570+
571+ Array literals and class instantiations are out of scope (PHP const
572+ expressions are limited).
573+
574+ ### Name generation
575+
576+ Generate a default name from the value:
577+
578+ - String: ` 'pending' ` → ` PENDING ` . ` 'order_status' ` → ` ORDER_STATUS ` .
579+ - Number: ` 200 ` → ` STATUS_200 ` or ` VALUE_200 ` .
580+ - Boolean: ` true ` → ` IS_ENABLED ` (weak heuristic, user will rename).
581+ - Fallback: ` CONSTANT ` with a numeric suffix if needed.
582+
583+ Use ` SCREAMING_SNAKE_CASE ` per PHP convention. If the generated name
584+ collides with an existing constant in the class, append a numeric suffix.
585+
586+ ### Insertion point
587+
588+ Insert the new constant declaration at the top of the class body, after
589+ any existing constant declarations (to keep constants grouped). Use the
590+ visibility of the surrounding context as a hint: if the literal appears
591+ in a public method, default to ` public const ` ; otherwise ` private const ` .
592+
593+ ### Duplicate replacement
594+
595+ Same approach as Extract Variable (§8): offer "this occurrence only"
596+ and "all N occurrences in this class". Textual equality is sufficient
597+ for literals.
598+
599+ ### Implementation
600+
601+ - Verify the selection is a literal expression node inside a class body.
602+ - Find the class declaration node and scan for existing constants.
603+ - Generate the constant name and check for collisions.
604+ - Determine the insertion point (after last existing constant, or at
605+ the top of the class body if none exist).
606+ - Build a ` WorkspaceEdit ` that:
607+ 1 . Inserts ` {visibility} const NAME = {value};\n ` at the insertion
608+ point with correct indentation.
609+ 2 . Replaces the selected literal with ` self::NAME ` .
610+ 3 . Optionally replaces other identical literals in the class.
611+
612+ ### Prerequisites
613+
614+ | Feature | What it contributes |
615+ | ---| ---|
616+ | Extract Function (§3) scope analysis | Class body traversal and constant name collision detection |
617+
618+ ---
619+
620+ ## 11. Update Docblock to Match Signature
621+ ** Impact: Medium · Effort: Medium**
622+
623+ When a function or method signature changes (parameters added, removed,
624+ reordered, or type hints updated), the docblock often falls out of sync.
625+ This code action regenerates or patches the ` @param ` , ` @return ` , and
626+ ` @throws ` tags to match the current signature.
627+
628+ ### Behaviour
629+
630+ - ** Trigger:** Cursor is on a function/method declaration that has an
631+ existing docblock. The code action appears when the docblock's ` @param `
632+ tags don't match the signature's parameters (by name, count, or order),
633+ or when the ` @return ` tag contradicts the return type hint.
634+ - ** Code action kind:** ` quickfix ` (when tags are clearly wrong) or
635+ ` source.fixAll.docblock ` for a broader sweep.
636+
637+ ### What gets updated
638+
639+ 1 . ** ` @param ` tags:**
640+ - Add missing ` @param ` for parameters present in the signature but
641+ absent from the docblock.
642+ - Remove ` @param ` for parameters no longer in the signature.
643+ - Reorder ` @param ` tags to match signature order.
644+ - Update the type if the signature has a type hint and the docblock
645+ type contradicts it (e.g. docblock says ` string ` , signature says
646+ ` int ` ). If the docblock type is _ more specific_ than the signature
647+ (e.g. docblock says ` non-empty-string ` , signature says ` string ` ),
648+ keep the docblock type (it's a refinement, not a contradiction).
649+ - Preserve existing descriptions after the type and variable name.
650+
651+ 2 . ** ` @return ` tag:**
652+ - If the signature has a return type hint and the docblock ` @return `
653+ contradicts it, update the type. Same refinement rule: keep the
654+ docblock type if it's more specific.
655+ - If the signature has a return type but no ` @return ` tag exists,
656+ do not add one (the type hint is sufficient). Only update or
657+ remove existing tags.
658+ - Remove ` @return void ` if redundant with a ` : void ` return type.
659+
660+ 3 . ** Preserve other tags:** ` @throws ` , ` @template ` , ` @deprecated ` ,
661+ ` @see ` , and any other tags are left untouched.
662+
663+ ### Edge cases
664+
665+ - ** Promoted constructor parameters:** Treat the same as regular
666+ parameters for ` @param ` purposes.
667+ - ** Variadic parameters:** ` ...$args ` matches ` @param type ...$args ` .
668+ - ** No existing docblock:** This action only patches existing docblocks.
669+ PHPDoc generation on ` /** ` (Sprint 5, item 24) handles creating new
670+ ones.
671+
672+ ### Implementation
673+
674+ - Parse the function signature to extract parameter names, types, and
675+ order, plus the return type.
676+ - Parse the existing docblock to extract ` @param ` and ` @return ` tags
677+ with their positions, types, variable names, and descriptions.
678+ - Diff the two lists to determine additions, removals, reorderings,
679+ and type updates.
680+ - Build a ` WorkspaceEdit ` with targeted ` TextEdit ` s that modify only
681+ the changed lines within the docblock, preserving formatting,
682+ indentation, and unchanged tags.
683+
684+ ### Prerequisites
685+
686+ | Feature | What it contributes |
687+ | ---| ---|
688+ | Docblock tag parsing (` docblock/tags.rs ` ) | Extracts existing ` @param ` /` @return ` tags with positions |
689+ | Parser (` parser/functions.rs ` ) | Extracts parameter names, types, and return type from the signature |
690+
691+ ---
692+
693+ ## 12. Change Visibility
694+ ** Impact: Low-Medium · Effort: Low**
695+
696+ Change the visibility of a method, property, constant, or class from the
697+ cursor position. Offers all applicable alternatives as separate code
698+ actions.
699+
700+ ### Behaviour
701+
702+ - ** Trigger:** Cursor is on (or inside) a method, property, constant, or
703+ promoted constructor parameter that has an explicit visibility modifier.
704+ - ** Code action kind:** ` refactor.rewrite ` .
705+ - ** Offered actions:** One action per alternative visibility. For a
706+ ` public ` method, offer "Make protected" and "Make private". For a
707+ ` private ` property, offer "Make protected" and "Make public".
708+
709+ ### What can be changed
710+
711+ - Methods: ` public ` ↔ ` protected ` ↔ ` private ` .
712+ - Properties (including promoted constructor parameters): same.
713+ - Constants: ` public ` ↔ ` protected ` ↔ ` private ` (PHP 8.1+).
714+ - Constructor visibility promotion: ` public function __construct(private string $name) ` —
715+ change ` private ` to ` protected ` or ` public ` .
716+
717+ ### Scope
718+
719+ This is a single-file edit. It does ** not** update call sites or
720+ subclass overrides in other files. If a user makes a public method
721+ private and there are external callers, they'll see errors from
722+ diagnostics or their static analyser. Cross-file visibility propagation
723+ is a possible follow-up but not required for the initial implementation.
724+
725+ ### Implementation
726+
727+ - Find the visibility keyword token for the member under the cursor.
728+ - Determine the current visibility.
729+ - For each alternative visibility, create a code action whose
730+ ` WorkspaceEdit ` replaces the visibility keyword token with the new one.
731+ - Handle the edge case of implicit public visibility (no keyword present)
732+ by inserting the keyword before the ` function ` /property token.
733+
734+ ### Prerequisites
735+
736+ None beyond basic AST navigation. This is one of the simplest possible
737+ code actions.
738+
739+ ---
740+
741+ ## 13. Generate Interface from Class
742+ ** Impact: Low-Medium · Effort: Medium**
743+
744+ Extract an interface from an existing class. The new interface contains
745+ method signatures for all public methods in the class, and the class is
746+ updated to implement it.
747+
748+ ### Behaviour
749+
750+ - ** Trigger:** Cursor is on a class declaration. The code action
751+ "Extract interface" appears.
752+ - ** Code action kind:** ` refactor.extract ` .
753+ - ** Result:** A new file is created containing the interface, and the
754+ original class is updated to add ` implements InterfaceName ` .
755+
756+ ### What gets extracted
757+
758+ - All ` public ` methods (excluding the constructor) become interface
759+ method signatures: visibility, name, parameters with types and
760+ defaults, and return type.
761+ - PHPDoc blocks from the extracted methods are copied to the interface
762+ (they often contain ` @param ` , ` @return ` , and ` @template ` tags that
763+ are essential for type information).
764+ - Class-level ` @template ` tags are copied if any extracted method
765+ references those template parameters.
766+ - Public constants are ** not** extracted (interface constants have
767+ different semantics and this is rarely what users want).
768+
769+ ### Naming
770+
771+ Default interface name: ` {ClassName}Interface ` . Place it in the same
772+ namespace and directory as the class. If the file uses PSR-4, the
773+ interface file path is derived from the namespace.
774+
775+ ### Implementation
776+
777+ - Parse the class to collect public method signatures and their
778+ docblocks.
779+ - Collect class-level ` @template ` tags if referenced by extracted
780+ methods.
781+ - Generate the interface source: namespace declaration, use imports
782+ needed by the method signatures, interface declaration with method
783+ stubs.
784+ - Build a ` WorkspaceEdit ` with two operations:
785+ 1 . ` CreateFile ` + ` TextEdit ` for the new interface file.
786+ 2 . ` TextEdit ` on the original class to add ` implements InterfaceName `
787+ (and a ` use ` import if the interface is in a different file, though
788+ by default it's the same namespace).
789+ - Format the generated interface to match the project's indentation
790+ style (detect from the source class).
791+
792+ ### Edge cases
793+
794+ - ** Class already implements interfaces:** Append to the existing
795+ ` implements ` list rather than replacing it.
796+ - ** Abstract methods:** Include them in the interface (they're already
797+ stubs).
798+ - ** Static methods:** Include them. Interfaces can declare static method
799+ signatures.
800+ - ** Generic classes:** If the class has ` @template T ` and a method
801+ returns ` T ` , the interface needs the same ` @template ` tag.
802+
803+ ### Prerequisites
804+
805+ | Feature | What it contributes |
806+ | ---| ---|
807+ | Parser (` parser/classes.rs ` ) | Extracts public method signatures with full type information |
808+ | Implement missing methods (§1) | Shared infrastructure for generating method stubs and ` implements ` clause editing |
0 commit comments