Skip to content
Open
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.0.0"
".": "2.0.1"
}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [2.0.1](https://github.com/rejmann/php-namespace-refactor/compare/php-namespace-refactor-v2.0.0...php-namespace-refactor-v2.0.1) (2026-07-31)


### 🐛 Bug Fixes

* **property-rename:** default mismatched property renames to on ([c8bb98f](https://github.com/rejmann/php-namespace-refactor/commit/c8bb98f283d66cd0cf18df810af871ac5e312667))


### ♻️ Code Refactoring

* **property-rename:** fold property renames into the batch update pass ([8b7ef0a](https://github.com/rejmann/php-namespace-refactor/commit/8b7ef0a68e72ed190c83090422d366428624624e))
* **workspace:** add polymorphic flag resolution for workspace settings ([fde3853](https://github.com/rejmann/php-namespace-refactor/commit/fde38531ab175cb018e12bd00f483dc7a1b8523a))

## [2.0.0](https://github.com/rejmann/php-namespace-refactor/compare/php-namespace-refactor-v1.9.2...php-namespace-refactor-v2.0.0) (2026-07-30)


Expand Down
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ Ideal for projects using PSR-4, making it easy to reorganize directories without

- Rename Properties (off by default): When a class is renamed, also rename its class-typed constructor properties (promoted or not, readonly or not) and their `$this->x` usages to match the new class name.

### 🩺 Diagnostics and quick fixes

Beyond the move/rename flow, the extension also watches files as you edit them and surfaces a few checks in the Problems panel, each individually toggleable:

- Namespace Mismatch Diagnostics: Warns when a file's declared namespace doesn't match its PSR-4 location, with a quick fix to correct it in place (no move required).

- Highlight Not Imported: Warns when a class is used in the file but not imported, whenever it resolves to exactly one class elsewhere in the workspace — with a quick fix to add the `use` statement. Ambiguous matches (the same class name found in more than one place) are left alone rather than guessed.

- Highlight Not Used: Flags `use` imports that are never referenced in the file, with a quick fix to remove them.

- Remove On Save / Sort On Save (off by default): Automatically remove unused imports and/or sort the remaining ones every time a PHP file is saved, independently of any move/rename operation. Sort order is configurable (natural, length, or alphabetical).

## Requirements

- PHP 7.4+
Expand All @@ -50,7 +62,13 @@ This extension contributes the following settings:
],
"phpNamespaceRefactor.rename": true,
"phpNamespaceRefactor.editFilesInBackground": true,
"phpNamespaceRefactor.renameProperties": false
"phpNamespaceRefactor.renameProperties": false,
"phpNamespaceRefactor.namespaceMismatchDiagnostics": true,
"phpNamespaceRefactor.highlightNotUsed": true,
"phpNamespaceRefactor.highlightNotImported": true,
"phpNamespaceRefactor.removeOnSave": false,
"phpNamespaceRefactor.sortOnSave": false,
"phpNamespaceRefactor.sortMode": "natural"
}
```

Expand Down Expand Up @@ -111,6 +129,46 @@ This extension contributes the following settings:

- Default: false.

**phpNamespaceRefactor.namespaceMismatchDiagnostics**

- Shows a warning and a quick fix when a file's declared namespace doesn't match its PSR-4 location, without requiring a move/rename to fix it.

- Default: true.

**phpNamespaceRefactor.highlightNotUsed**

- Shows a hint and a quick fix for `use` imports that are never referenced in the file.

- Default: true.

**phpNamespaceRefactor.highlightNotImported**

- Shows a warning and a quick fix for classes used in the file that resolve to exactly one class elsewhere in the workspace but aren't imported yet. If the class name matches more than one location in the workspace, it's left alone rather than guessed.

- Default: true.

**phpNamespaceRefactor.removeOnSave**

- Automatically removes unused `use` imports every time a PHP file is saved, independently of any move/rename operation.

- Default: false.

**phpNamespaceRefactor.sortOnSave**

- Automatically sorts `use` imports every time a PHP file is saved, using the order configured in `phpNamespaceRefactor.sortMode`.
- When combined with `removeOnSave`, both happen together as a single edit.

- Default: false.

**phpNamespaceRefactor.sortMode**

- Sort order used by `phpNamespaceRefactor.sortOnSave`. One of:
- `natural`: case-insensitive, numeric-aware order (e.g. `Item2` before `Item10`).
- `length`: shortest `use` statement first.
- `alphabetical`: strict character-by-character order.

- Default: "natural".

## Documentation

For architecture, internals, and troubleshooting notes, see [./docs/](./docs/README.md).
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Documentation to support development and troubleshooting of the PHP Namespace Re
- **[Namespace rename](./operations/namespace-rename.md)** — F2 on `namespace Foo\Bar;`
- **[Class rename](./operations/class-rename.md)** — F2 on `class Foo`/`interface Foo`/`trait Foo`
- **[File move](./operations/file-move.md)** — drag-and-drop in the Explorer (and the convergence point of the two flows above)
- **[Diagnostics and quick fixes](./diagnostics.md)** — namespace-mismatch/unused-import/missing-import checks in the Problems panel, and the `removeOnSave`/`sortOnSave` save-time edits

## Infrastructure

Expand Down
10 changes: 9 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@ All keys are centralized in `ConfigKeys` (`src/domain/workspace/ConfigurationLoc
| `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 `MultiFileReferenceUpdater`/`PropertyRenameOperation` |
| `phpNamespaceRefactor.namespaceMismatchDiagnostics` | `NAMESPACE_MISMATCH_DIAGNOSTICS` | `boolean` | `true` | `NamespaceDiagnosticsBuilder` |
| `phpNamespaceRefactor.highlightNotUsed` | `HIGHLIGHT_NOT_USED` | `boolean` | `true` | `UnusedImportDiagnosticsBuilder` |
| `phpNamespaceRefactor.highlightNotImported` | `HIGHLIGHT_NOT_IMPORTED` | `boolean` | `true` | `MissingImportDiagnosticsBuilder` |
| `phpNamespaceRefactor.removeOnSave` | `REMOVE_ON_SAVE` | `boolean` | `false` | `UseStatementBlockEditsBuilder` |
| `phpNamespaceRefactor.sortOnSave` | `SORT_ON_SAVE` | `boolean` | `false` | `UseStatementBlockEditsBuilder` |
| `phpNamespaceRefactor.sortMode` | `SORT_MODE` | `"natural" \| "length" \| "alphabetical"` | `"natural"` | `UseStatementBlockEditsBuilder`, sorting logic in `UseStatementSorter` |

See [diagnostics.md](./diagnostics.md) for how the last six are wired together (the three diagnostics share one subscriber; the two save-time settings share one edit builder to avoid overlapping edits).

## How configuration is read

Three classes access `workspace.getConfiguration('phpNamespaceRefactor')`, each with a distinct purpose:

- **`ConfigurationLocator`** (`src/domain/workspace/ConfigurationLocator.ts`) — generic read, used for settings of any type (`ignoredDirectories`, `additionalExtensions`)
- **`FeatureFlagManager`** (`src/domain/workspace/FeatureFlagManager.ts`) — specialized `boolean` read, with `defaultValue = true`. Used for every plain on/off flag (`autoImportNamespace`, `removeUnusedImports`, `rename`, `editFilesInBackground`)
- **`FeatureFlagManager`** (`src/domain/workspace/FeatureFlagManager.ts`) — specialized `boolean` read, with `defaultValue = true`. Used for every plain on/off flag (`autoImportNamespace`, `removeUnusedImports`, `rename`, `editFilesInBackground`, `namespaceMismatchDiagnostics`, `highlightNotUsed`, `highlightNotImported`) — `removeOnSave` and `sortOnSave` also go through it, but pass `defaultValue: false` explicitly since they mutate the file on every save and shouldn't be on by default
- **`PropertyRenameSettingsResolver`** (`src/domain/property/PropertyRenameSettingsResolver.ts`) — the one setting whose raw value isn't a plain boolean; see [`phpNamespaceRefactor.renameProperties`](#phpnamespacerefactorrenameproperties) below

None of the three caches the `WorkspaceConfiguration` — `ConfigurationLocator`/`FeatureFlagManager` read `workspace.getConfiguration()` in their constructor, and `PropertyRenameSettingsResolver` reads through a fresh `ConfigurationLocator` on every `resolve()` call. All three are `@injectable()` (not singleton), so a fresh read happens on every `container.resolve()`. This means a change to the user's configuration is picked up on the next operation, with no need to reload the window.
Expand Down
57 changes: 57 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Diagnostics and quick fixes

**Files:** `src/app/services/NamespaceDiagnosticsBuilder.ts`, `src/app/services/UnusedImportDiagnosticsBuilder.ts`, `src/app/services/MissingImportDiagnosticsBuilder.ts`, `src/app/services/MissingImportResolver.ts`, `src/app/services/SingleImportInserter.ts`, `src/app/services/UseStatementBlockEditsBuilder.ts`, `src/app/subscribers/NamespaceDiagnosticsSubscriber.ts`, `src/app/commands/NamespaceCodeActionProvider.ts`, `src/app/commands/UnusedImportCodeActionProvider.ts`, `src/app/commands/MissingImportCodeActionProvider.ts`, `src/infra/vscode/NamespaceDiagnosticCollection.ts`

## Responsibility

Separate from the move/rename/F2 flows (see [architecture.md](./architecture.md#main-flows)), the extension also watches PHP documents as they're opened, saved, and closed, and surfaces three independent checks in the Problems panel. Each has its own feature flag (see [configuration.md](./configuration.md)) and its own `Diagnostic.code`, so a `CodeActionProvider` only ever reacts to the diagnostics it knows how to fix.

| Check | Builder | Diagnostic code | Severity | Quick fix (`CodeActionProvider`) |
|---|---|---|---|---|
| Declared namespace doesn't match PSR-4 location | `NamespaceDiagnosticsBuilder` | `namespace-mismatch` | Warning | `NamespaceCodeActionProvider` — replaces the `namespace ...;` line in place |
| `use` import never referenced in the file | `UnusedImportDiagnosticsBuilder` | `unused-import` | Hint (`DiagnosticTag.Unnecessary`, fades the text) | `UnusedImportCodeActionProvider` — deletes the whole line |
| Class used but not imported, resolves to exactly one class in the workspace | `MissingImportDiagnosticsBuilder` | `missing-import` | Warning | `MissingImportCodeActionProvider` — inserts a `use` statement via the `phpNamespaceRefactor.insertMissingImport` command |

## Event flow

`NamespaceDiagnosticsSubscriber` (registered in `extension.ts`) is the single entry point that runs all three builders and merges their output into one `NamespaceDiagnosticCollection` (a thin wrapper around `languages.createDiagnosticCollection`):

```
workspace.onDidOpenTextDocument → subscriber.handle(document)
workspace.onDidSaveTextDocument → subscriber.handle(document)
workspace.onDidCloseTextDocument → subscriber.clear(document)
workspace.textDocuments (on activate) → subscriber.handle(document), so already-open files get diagnostics immediately
```

There's no `onDidChangeActiveTextEditor`/keystroke-level trigger on purpose — recomputing on every keystroke would be noisy and unnecessary; open + save covers the practical editing workflow.

## Namespace mismatch: why the range is trimmed

`NAMESPACE_DECLARATION_REGEX` (`src/domain/namespace/PhpPatterns.ts`) allows leading blank lines via `\s*` — other call sites (`MovedFileNamespaceUpdater`) rely on that to normalize spacing when replacing the line. `NamespaceDiagnosticsBuilder` trims that leading whitespace off before building the diagnostic's `Range`, otherwise the warning underline (and the quick fix's replace range) would start on the blank line above `namespace ...;` instead of the line itself.

## Missing import: resolving a bare identifier to a class

`MissingImportCandidateLocator` (domain, pure) scans the document text for bare capitalized identifiers that aren't already namespace-qualified, aren't right after `::`/`->` (a member access, not a class reference), aren't already imported/aliased, and aren't the file's own class name. It also blanks out the `namespace ...;` declaration itself before scanning (same-length space padding, so every other match's offset stays correct) — otherwise `namespace App;` would have its own `App` segment mistaken for a used-but-unimported identifier.

`MissingImportResolver` (app) takes each candidate and looks it up via `NamespaceIndex.findClassLocations()` (new method — derives each indexed file's class name from its file name, the same convention as `WorkspacePathResolver.extractClassNameFromPath`). A candidate is only ever flagged when:

- exactly one file in the workspace declares a class by that name (zero matches = unresolved, more than one = ambiguous — both are skipped rather than guessed, same philosophy as the ambiguous-property-rename skip in `PropertyRenameOperation`), and
- that one match isn't already in the file's own declared namespace (already in scope without an import, same rule `MissingClassImporter` applies for the move flow)

This means built-in/global PHP classes (`Exception`, `Closure`, etc.) are never flagged — they're not in the workspace's own namespace index, so they simply don't resolve.

The quick fix (`MissingImportCodeActionProvider`) re-runs `MissingImportResolver` against the document rather than stashing the resolved FQCN on the diagnostic, then triggers the `phpNamespaceRefactor.insertMissingImport` command, which delegates to `SingleImportInserter` — a thin wrapper around the same `UseStatementLocator`/`UseStatementCreator`/`UseStatementInjector` trio `MissingClassImporter` already uses for the move flow, so insertion point, duplicate-avoidance, and blank-line handling all stay consistent between the two features.

## Save-time edits: `removeOnSave` and `sortOnSave`

Unlike the three diagnostics above, `removeOnSave` and `sortOnSave` don't go through the Problems panel — they mutate the file directly on save, via `workspace.onWillSaveTextDocument` + `event.waitUntil(...)`. This is deliberate: contributing edits through `waitUntil` folds them into the save that's already happening, so the file doesn't get saved once, then edited again into a dirty state (which is what using `onDidSaveTextDocument` + `workspace.applyEdit` would cause).

Both behaviors are computed by a single class, `UseStatementBlockEditsBuilder`, instead of two independent subscribers. They act on the same contiguous block of `use` lines, so two separate edits (one deleting unused lines, one reordering the rest) would overlap — VS Code rejects overlapping edits within one `TextEdit[]`. Instead, the builder:

1. Locates every `use` line (`USE_STATEMENT_REGEX`) and checks they're on consecutive document lines — if anything is interleaved (a comment, a blank line), it bails out entirely rather than risk reordering across it.
2. If `removeOnSave` is on, drops the ones `UnusedUseStatementLocator` reports as unreferenced.
3. If `sortOnSave` is on, reorders what's left via `UseStatementSorter`, using the mode from `phpNamespaceRefactor.sortMode` (`natural` | `length` | `alphabetical`).
4. If the result is identical to the original block, returns no edit at all.
5. Otherwise, replaces the whole original block (first `use` line to last) with the final block in a single `TextEdit`.

If every import ends up removed, this can leave one blank line behind where the block used to be — a minor cosmetic gap, not a correctness issue, consistent with the regex-based (not AST-based) approach used throughout this codebase.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 41 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "php-namespace-refactor",
"displayName": "PHP Namespace Refactor",
"description": "PHP Namespace Refactor: Extension for Visual Studio Code that automatically refactors namespace and references when moving PHP files between directories.",
"version": "2.0.0",
"version": "2.0.1",
"author": {
"name": "rejman",
"url": "https://github.com/rejmann"
Expand Down Expand Up @@ -125,6 +125,46 @@
},
"additionalProperties": false,
"description": "Rename constructor-typed properties (promoted or not, readonly or not) - and their $this->x usages - to match the class name when renaming a class. Set to true to enable everything, including renaming mismatched names, or to an object like { \"renameMismatchedNames\": false } to opt out of just that behavior."
},
"phpNamespaceRefactor.namespaceMismatchDiagnostics": {
"type": "boolean",
"default": true,
"description": "Show a warning and a quick fix when a file's declared namespace doesn't match its PSR-4 location, without requiring a move/rename to fix it."
},
"phpNamespaceRefactor.highlightNotUsed": {
"type": "boolean",
"default": true,
"description": "Show a hint and a quick fix for \"use\" imports that are never referenced in the file."
},
"phpNamespaceRefactor.highlightNotImported": {
"type": "boolean",
"default": true,
"description": "Show a warning and a quick fix for classes used in the file that resolve to exactly one class elsewhere in the workspace but aren't imported yet."
},
"phpNamespaceRefactor.removeOnSave": {
"type": "boolean",
"default": false,
"description": "Automatically remove unused \"use\" imports every time a PHP file is saved, independently of any move/rename operation."
},
"phpNamespaceRefactor.sortOnSave": {
"type": "boolean",
"default": false,
"description": "Automatically sort \"use\" imports every time a PHP file is saved, using the order configured in phpNamespaceRefactor.sortMode."
},
"phpNamespaceRefactor.sortMode": {
"type": "string",
"enum": [
"natural",
"length",
"alphabetical"
],
"enumDescriptions": [
"Natural order (case-insensitive, numeric-aware), e.g. Item2 before Item10.",
"Shortest \"use\" statement first.",
"Strict character-by-character alphabetical order."
],
"default": "natural",
"description": "Sort order used by phpNamespaceRefactor.sortOnSave."
}
}
}
Expand Down
Loading