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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- BEAR and Ray gutter icons now use transparent backgrounds.
- Incoming resource relation gutters moved from the resource class name to the target resource method (`#[Embed]` always maps to `onGet`; `#[Link]` maps from its `method` argument and defaults to `onGet`).
- Replaced the deprecated `Project#getBaseDir()` with `ProjectUtil.guessProjectDir()` (with null guards) across the resource / router / SQL / JSON Schema goto handlers and the resource index
- Replaced the remaining deprecated/internal API usages reported by the plugin verifier against IntelliJ IDEA 2026.2.1: `FilenameIndex.getFilesByName()` → `FilenameIndex.getVirtualFilesByName()` and internal `PhpType.from()` → public `new PhpType().add()` in the resource method type provider
- Refreshed README and plugin description metadata, including current feature wording and Marketplace links
- Removed stale README TODOs and legacy Php Annotations Plugin references from public documentation
- Updated the MIT license notice to cover 2015-2026 Shingo Kumagai and contributors
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/idea/bear/sunday/body/BodyTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ static List<ShapeField> shapeFields(BodyType type) {
return null;
}

static List<BodyType> unionTypes(BodyType type) {
public static List<BodyType> unionTypes(BodyType type) {
if (type instanceof UnionType unionType) {
return unionType.types();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import com.jetbrains.php.lang.psi.elements.Variable;
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4;
import idea.bear.sunday.body.BodyType;
import idea.bear.sunday.body.BodyTypeCollector;
import idea.bear.sunday.body.BodyTypeDeclaration;
import idea.bear.sunday.body.BodyTypes;
import idea.bear.sunday.util.UriUtil;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -109,18 +111,34 @@ public Collection<? extends PhpNamedElement> getBySignature(String expression, S
private @Nullable PhpType completeRequest(SignedResourceRequest request, Project project) {
if (SIGNATURE_RESOURCE.equals(request.kind())) {
return resolveResourceClass(project, request.uri())
.map(phpClass -> PhpType.from(phpClass.getFQN()))
.map(phpClass -> new PhpType().add(phpClass.getFQN()))
.orElse(null);
}
if (SIGNATURE_BODY.equals(request.kind())) {
return resolveBodyType(project, request)
.map(declaration -> PhpType.from(declaration.bodyType().render(), PhpType._NULL))
.map(declaration -> bodyPhpType(declaration.bodyType()))
.orElse(null);
}

return null;
}

private static PhpType bodyPhpType(BodyType bodyType) {
// PhpType.add(String) stores the argument as one opaque type, so a rendered union
// ("array{…}|array{…}") must be added component by component to stay a real union.
PhpType phpType = new PhpType();
List<BodyType> unionParts = BodyTypes.unionTypes(bodyType);
if (unionParts == null) {
phpType.add(bodyType.render());
} else {
for (BodyType part : unionParts) {
phpType.add(part.render());
}
}

return phpType.add(PhpType._NULL);
}

private Optional<BodyTypeDeclaration> resolveBodyType(Project project, SignedResourceRequest request) {
return resolveResourceClass(project, request.uri())
.flatMap(bodyTypeCollector::collect)
Expand Down Expand Up @@ -305,9 +323,14 @@ private static Optional<PhpClass> resolveResourceClassFromFilenameIndex(Project
String fileName = className + ".php";
String expectedSuffix = "/" + relPath;
try {
for (PsiFile psiFile : FilenameIndex.getFilesByName(project, fileName, GlobalSearchScope.allScope(project))) {
VirtualFile virtualFile = psiFile.getVirtualFile();
if (virtualFile == null || !virtualFile.getPath().replace('\\', '/').endsWith(expectedSuffix)) {
PsiManager psiManager = PsiManager.getInstance(project);
for (VirtualFile virtualFile : FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.allScope(project))) {
if (!virtualFile.getPath().replace('\\', '/').endsWith(expectedSuffix)) {
continue;
}

PsiFile psiFile = psiManager.findFile(virtualFile);
if (psiFile == null) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,48 @@ public function onPut(): void
assertTrue(completedType.isNullable(), completedType::toString);
}

@Test
void addsUnionBodyTypeComponentsIndividually() {
addPhysicalPhpFile("src/Resource/App/Article.php", """
<?php
namespace MyVendor\\MyProject\\Resource\\App;

final class Article extends \\BEAR\\Resource\\ResourceObject
{
public function onGet(bool $found): static
{
if ($found) {
$this->body = ['id' => 1];

return $this;
}

$this->body = ['error' => 'not found'];

return $this;
}
}
""");
PsiFile caller = fixture.addFileToProject("src/Resource/App/Caller.php", """
<?php
namespace MyVendor\\MyProject\\Resource\\App;

final class Caller
{
public function onGet(): void
{
$body = $this->resource->get('app://self/article')->body;
}
}
""");

PhpType completedType = completedBodyType(caller);

assertTrue(completedType.getTypes().contains("array{id: int}"), completedType::toString);
assertTrue(completedType.getTypes().contains("array{error: string}"), completedType::toString);
assertTrue(completedType.getTypes().stream().noneMatch(type -> type.contains("|")), completedType::toString);
}

@Test
void narrowsDirectResourceBodyAccess() {
addPhysicalPhpFile("src/Resource/App/Article.php", """
Expand Down
Loading