Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,18 @@ This extension contributes the following settings:

**phpNamespaceRefactor.renameProperties**

- When a class is renamed, also renames its class-typed constructor properties (promoted or not, readonly or not) and every `$this->x` usage to match the new class name — e.g. `private Test $teste` becomes `private NewTest $newTest` when `Teste` is renamed to `Novo`.
- When a class is renamed, also renames its class-typed constructor properties (promoted or not, readonly or not) and every `$this->x` usage to match the new class name — e.g. `private Test $test` becomes `private NewTest $newTest` when `Test` is renamed to `NewTest`.
- If more than one property shares the same type in a constructor, the file is skipped rather than guessing which one to rename.
- Accepts either a boolean or an object:
```json
"phpNamespaceRefactor.renameProperties": true
```
```json
"phpNamespaceRefactor.renameProperties": {
"renameMismatchedNames": true
"renameMismatchedNames": false
}
```
Setting an object automatically enables the feature. `renameMismatchedNames` (default `false`) additionally renames properties whose current name doesn't already match the class name on purpose (e.g. `private Test $service` becomes `private NewTest $newTest`); without it, only properties already named after the old class are renamed.
Setting `true` (or an empty object) enables the feature with every child behavior on by default, including `renameMismatchedNames` — properties whose current name doesn't already match the class name (e.g. `private Test $service`) get renamed too. Use the object form only to dial a specific child back to `false`.

- Default: false.

Expand Down
16 changes: 8 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ All keys are centralized in `ConfigKeys` (`src/domain/workspace/ConfigurationLoc
| `phpNamespaceRefactor.additionalExtensions` | `ADDITIONAL_EXTENSIONS` | `string[]` | `["php"]` | `FileExtensionResolver` and `WorkspaceIndex` |
| `phpNamespaceRefactor.rename` | `RENAME` | `boolean` | `true` | The `phpNamespaceRefactor.rename` command (`extension.ts`) and the F2 keybinding's `when` clause (`package.json`) |
| `phpNamespaceRefactor.editFilesInBackground` | `EDIT_FILES_IN_BACKGROUND` | `boolean` | `true` | `FileEditApplier` |
| `phpNamespaceRefactor.renameProperties` | `RENAME_PROPERTIES` | `boolean \| { renameMismatchedNames?: boolean }` | `false` | `PropertyRenameSettingsResolver`, consumed by `FileMoveOperation`/`PropertyRenameOperation` |
| `phpNamespaceRefactor.renameProperties` | `RENAME_PROPERTIES` | `boolean \| { renameMismatchedNames?: boolean }` | `false` | `PropertyRenameSettingsResolver`, consumed by `MultiFileReferenceUpdater`/`PropertyRenameOperation` |

## How configuration is read

Expand Down Expand Up @@ -43,7 +43,7 @@ Normalized by `normalizeExtensions()` (`src/infra/utils/extensions.ts`): strips

## `phpNamespaceRefactor.renameProperties`

Master switch for `PropertyRenameOperation` — off by default, unlike the extension's other flags, because it renames identifiers (constructor parameters, promoted/non-promoted properties, `$this->x` usages), not just type hints. Two conditions gate it: the class itself must have actually been renamed (`oldClassName !== newClassName` — a plain directory move is a no-op), and only files `NamespaceBatchUpdater`/`MultiFileReferenceUpdater` already determined were affected by that specific rename are ever touched, never an independent workspace-wide scan — so an unrelated class in another namespace that happens to share a short name is never at risk.
Master switch for `PropertyRenameOperation` — off by default, unlike the extension's other flags, because it renames identifiers (constructor parameters, promoted/non-promoted properties, `$this->x` usages), not just type hints. Two conditions gate it: the class itself must have actually been renamed (`oldClassName !== newClassName` — a plain directory move is a no-op), and only files `MultiFileReferenceUpdater` already determined were affected by that specific rename are ever touched, never an independent workspace-wide scan — so an unrelated class in another namespace that happens to share a short name is never at risk. The rename is folded into the very same per-file `WorkspaceEdit` as the class-name replacement itself, not a separate later pass.

For each candidate file, `ClassTypedPropertyLocator` looks for the single constructor property typed as the renamed class (promoted or not, readonly or not); if more than one property shares that type, the file is skipped entirely rather than guessing which one to rename — see [file-move.md](./operations/file-move.md#3-property-rename-propertyrenameoperation-optional).

Expand All @@ -53,16 +53,16 @@ This one setting doubles as its own sub-option, via `PropertyRenameSettingsResol

```jsonc
"phpNamespaceRefactor.renameProperties": false // disabled (default)
"phpNamespaceRefactor.renameProperties": true // enabled, mismatched names left alone
"phpNamespaceRefactor.renameProperties": {} // enabled, mismatched names left alone
"phpNamespaceRefactor.renameProperties": { "renameMismatchedNames": true } // enabled, mismatched names renamed too
"phpNamespaceRefactor.renameProperties": true // enabled, mismatched names renamed too (default child behavior)
"phpNamespaceRefactor.renameProperties": {} // enabled, mismatched names renamed too (default child behavior)
"phpNamespaceRefactor.renameProperties": { "renameMismatchedNames": false } // enabled, but mismatched names left alone
```

`renameMismatchedNames` controls what happens when a property's current name doesn't already follow the class-name convention:

- unset/`false` (default) — a property is only renamed if its name already matches the *old* class name (e.g. `$teste` for `Teste`); a property named something else on purpose (e.g. `$service`) is left untouched
- `true` — mismatched names are renamed too, to match the *new* class name (e.g. `$service` → `$novo`)
- unset/`true` (default whenever the feature is enabled) — mismatched names are renamed too, to match the *new* class name (e.g. `$service` → `$newTest`)
- `false` — a property is only renamed if its name already matches the *old* class name (e.g. `$test` for `Test`); a property named something else on purpose (e.g. `$service`) is left untouched

Any object value implies the feature is enabled — `false` is the only way to turn it off; there's no `{ "enabled": false }` form.
Turning the feature on — via a bare `true` or an object — defaults every child behavior to `true` as well; the object form exists only to dial a specific child back to `false`. Any object value implies the feature is enabled — `false` is the only way to turn it off; there's no `{ "enabled": false }` form.

**Why one polymorphic setting instead of two plain booleans:** VS Code's settings schema doesn't allow a key to be both a leaf value and the parent of another key. An earlier version declared `phpNamespaceRefactor.renameProperties` (boolean) alongside `phpNamespaceRefactor.renameProperties.renameMismatchedNames` (boolean) as two separate keys — VS Code detected the conflict, logged `Ignoring phpNamespaceRefactor.renameProperties.renameMismatchedNames as phpNamespaceRefactor.renameProperties is false` in the console, and silently resolved **both** settings to `false` regardless of what the user configured. Collapsing them into a single `boolean | object` setting sidesteps the conflict entirely, since there's only ever one registered key.
2 changes: 1 addition & 1 deletion docs/operations/class-rename.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ It then delegates the rename to `FileRenameHandler.create()`, which triggers VS
- Update the `namespace` declaration in the file
- Update the class name inside the file (via `ClassNameUpdater`)
- Update every `use` statement referencing the class throughout the project
- Optionally, rename class-typed constructor properties (and their `$this->x` usages) in the affected files to match the new class name — only when `renameProperties` is enabled, see `PropertyRenameOperation` in [file-move.md](./file-move.md#3-property-rename-propertyrenameoperation-optional)
- Optionally, rename class-typed constructor properties (and their `$this->x` usages) in the affected files to match the new class name — only when `renameProperties` is enabled, folded into the same pass as the reference update; see `PropertyRenameOperation` in [file-move.md](./file-move.md#2-namespace-and-reference-update-namespacebatchupdater)

## Difference from a direct Explorer rename

Expand Down
41 changes: 22 additions & 19 deletions docs/operations/file-move.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ onDidRenameFiles (VS Code event)
→ FileMoveOperation.execute(files)
→ DirectoryMovedFilesResolver.execute() 1. expands directory moves into per-file moves
→ for each .php file:
→ NamespaceBatchUpdater.execute() 2. updates namespace/class + references
→ PropertyRenameOperation.execute() 3. (optional) renames class-typed properties to match the new class name
→ MissingClassImporter.execute() 4. (optional) auto-imports classes from the old directory
→ ImportRemover.execute() 5. (optional, checked internally) removes stale imports
→ NamespaceBatchUpdater.execute() 2. updates namespace/class + references, folding in
(optional) class-typed property renaming
→ MissingClassImporter.execute() 3. (optional) auto-imports classes from the old directory
→ ImportRemover.execute() 4. (optional, checked internally) removes stale imports
```

### Serialized queue
Expand Down Expand Up @@ -47,7 +47,7 @@ For each moved file, `src/app/services/NamespaceBatchUpdater.ts`:
5. If the namespace declaration wasn't found/changed (step 4 returns `false`), the operation stops here — there's nothing to propagate for a file with no `namespace` declaration
6. Otherwise, calls `MultiFileReferenceUpdater` to propagate the change across the rest of the project

`NamespaceBatchUpdater.execute()` returns the list of files `MultiFileReferenceUpdater` determined were affected (empty when it never ran, e.g. no `namespace` declaration to key off). `FileMoveOperation` forwards that same list into step 3 below, so property renaming only ever touches files this specific rename actually reached — never an independent, broader scan.
`NamespaceBatchUpdater.execute()` returns the list of files `MultiFileReferenceUpdater` determined were affected (empty when it never ran, e.g. no `namespace` declaration to key off).

#### `MultiFileReferenceUpdater` — how affected files are found

Expand All @@ -61,28 +61,31 @@ The union of both lists is the final set. This means even an empty/stale index n
For each affected file, it replaces via regex:
- every occurrence of the old fully-qualified namespace with the new one
- if the **class name** also changed, every occurrence of the old name with the new one (excluding spans already covered by the namespace substitution, to avoid overlapping ranges)
- if `renameProperties` resolves to enabled (off by default — see [configuration.md](../configuration.md#phpnamespacerefactorrenameproperties)) and the class was actually renamed, folds in a class-typed constructor property rename for that same file (see below) — into the *same* `WorkspaceEdit`, not a separate later pass
- if the file had no occurrence of the old namespace at all (i.e. didn't match either case above) but sits in the same directory as the moved file, it tries to insert a new `use` statement

Every edit across every affected file is accumulated into a **single `WorkspaceEdit`** before being applied (`FileEditApplier.apply`), so the whole refactor shows up as one undo-stack entry in VS Code instead of one edit per file — see [issue #72](https://github.com/rejmann/php-namespace-refactor/issues/72).
The renamed file itself (`newUri`) is excluded from the above (it's the source of the rename, not a consumer), but still gets its own property-rename check afterward — covers the rare case of a class holding a property typed as itself (e.g. a linked-list node).

At the end, `MultiFileReferenceUpdater` always calls `ImportRemover.execute({ uri: newUri })` for the moved file itself (in addition to the call `FileMoveOperation` already makes in step 5 below — both are no-ops if the flag is disabled or there's nothing to remove).
Every edit across every affected file — namespace, class name, **and** property renaming — is accumulated into a **single `WorkspaceEdit`** before being applied once (`FileEditApplier.apply`), so the whole refactor (including property renaming) shows up as one undo-stack entry in VS Code instead of one edit per file, and each file is only opened/saved once — see [issue #72](https://github.com/rejmann/php-namespace-refactor/issues/72).

### 3. Property rename (`PropertyRenameOperation`, optional)
At the end, `MultiFileReferenceUpdater` always calls `ImportRemover.execute({ uri: newUri })` for the moved file itself (in addition to the call `FileMoveOperation` already makes in step 4 below — both are no-ops if the flag is disabled or there's nothing to remove).

Only runs if `renameProperties` resolves to enabled (off by default — see [configuration.md](../configuration.md#phpnamespacerefactorrenameproperties)). `PropertyRenameSettingsResolver` reads the raw setting (`boolean | { renameMismatchedNames?: boolean }`) once in `FileMoveOperation` and turns it into `{ enabled, renameMismatchedNames }`; only `enabled` gates whether this step runs at all. Also only acts when the class itself was actually renamed (`oldClassName !== newClassName`); a plain move to a different directory with the same class name is a no-op.
##### Property renaming (`PropertyRenameOperation`, optional, folded in above)

1. Derives the expected old/new property names from the class names (`PropertyNameResolver`, e.g. `Teste` → `teste`)
2. Builds the candidate file list from the `affectedFiles` `FileMoveOperation` received back from `NamespaceBatchUpdater` (step 2's output), plus the moved file itself — deliberately **not** an independent workspace-wide scan, so a differently-namespaced class that happens to share a short name is never touched
3. For each candidate file whose text contains the new class name, `ClassTypedPropertyLocator` looks for the constructor property typed as that class — promoted or not, readonly or not, confirming a non-promoted parameter by checking for a `$this->x = $x;` assignment in the constructor body (see `ConstructorSpanFinder` for how the constructor's parameter list and body are located via brace/paren matching). The property's own declaration line doesn't need a type hint to be found this way — a legacy `private $x;` typed only via a `@var ClassName` docblock is still matched and renamed, via `PropertyDeclarationPattern`, once the constructor param already confirmed what class it holds
`PropertyRenameSettingsResolver` reads the raw setting (`boolean | { renameMismatchedNames?: boolean }`) once per `MultiFileReferenceUpdater.execute()` call and turns it into `{ enabled, renameMismatchedNames }`; only `enabled` gates whether this runs at all. Also only acts when the class itself was actually renamed (`oldClassName !== newClassName`); a plain move to a different directory with the same class name is a no-op.

1. Derives the expected old/new property names from the class names (`PropertyNameResolver`, e.g. `Test` → `test`)
2. Runs for every file `MultiFileReferenceUpdater` already determined was affected (plus the moved file itself) — deliberately **not** an independent workspace-wide scan, so a differently-namespaced class that happens to share a short name is never touched
3. For each candidate file, `ClassTypedPropertyLocator` looks for the constructor property typed as the renamed class — checking whichever of the old/new class name is actually present in that file's text at this point in the pass (still the old name for files whose class-name replacement is only queued, not yet applied) — promoted or not, readonly or not, confirming a non-promoted parameter by checking for a `$this->x = $x;` assignment in the constructor body (see `ConstructorSpanFinder` for how the constructor's parameter list and body are located via brace/paren matching). The property's own declaration line doesn't need a type hint to be found this way — a legacy `private $x;` typed only via a `@var ClassName` docblock is still matched and renamed, via `PropertyDeclarationPattern`, once the constructor param already confirmed what class it holds
4. If more than one property shares that type in the same file, it's ambiguous — the file is skipped entirely rather than guessing
5. If exactly one property is found: it's renamed when its current name already matches the *old* class-name convention, or — only when `renameMismatchedNames` also resolved to `true` — regardless of what it was named before
6. Renaming rewrites the constructor parameter/property declaration and every `$this->x` usage in that file, accumulated into a single `WorkspaceEdit` (its own undo stop, separate from `MultiFileReferenceUpdater`'s)
6. Renaming rewrites the constructor parameter/property declaration and every `$this->x` usage in that file, added to the same shared `WorkspaceEdit` as the class-name replacement

### 4. Auto-import of classes from the source directory (`MissingClassImporter`, optional)
### 3. Auto-import of classes from the source directory (`MissingClassImporter`, optional)

Only runs if the `autoImportNamespace` flag is enabled. Lists the `.php` files still left in the source directory, checks which classes from those files are used in the moved file's text but not imported, and inserts the corresponding `use` statements.

### 5. Removing stale imports (`ImportRemover`)
### 4. Removing stale imports (`ImportRemover`)

Unlike the other flags, the `removeUnusedImports` check happens **inside** `ImportRemover` itself (not in `FileMoveOperation`) — so it's always called, but returns immediately if the flag is disabled.

Expand All @@ -92,10 +95,10 @@ When enabled: it collects the class names declared in the other files of the mov

| Flag | Behavior |
|---|---|
| `renameProperties` (`true`/`{}`) | Enables step 3 (renaming class-typed constructor properties to match the new class name) |
| `renameProperties: { renameMismatchedNames: true }` | Extends step 3 to also rename properties whose name doesn't already match the class-name convention |
| `autoImportNamespace` | Enables step 4 (auto-import of classes from the old directory) |
| `removeUnusedImports` | Enables the import removal in step 5 (checked inside `ImportRemover`) |
| `renameProperties` (`true`/`{}`) | Enables property renaming, folded into step 2 (renaming class-typed constructor properties to match the new class name); `true`/`{}` also renames mismatched names by default (see below) |
| `renameProperties: { renameMismatchedNames: false }` | Opts out of renaming properties whose name doesn't already match the class-name convention (on by default whenever the feature is enabled) |
| `autoImportNamespace` | Enables step 3 (auto-import of classes from the old directory) |
| `removeUnusedImports` | Enables the import removal in step 4 (checked inside `ImportRemover`) |
| `editFilesInBackground` | Doesn't change what's edited, only whether touched files open a tab in the editor or are saved silently — see [configuration.md](../configuration.md) |

## Error handling
Expand Down
Loading
Loading