xp-framework / compiler

Compiles future PHP to today's PHP.
19 stars 0 forks source link

::namespace #178

Open thekid opened 8 months ago

thekid commented 8 months ago

How about using ::namespace for namespace resolution just like ::class?

Example:

<?php namespace de\thekid\dialog;

// Currently, using the dotted syntax
$api= new RestApi(new ResourcesIn('de.thekid.dialog.api'));

// The PHP way
$api= new RestApi(new ResourcesIn(__NAMESPACE__.'\\api'));

// Suggestion
$api= new RestApi(new ResourcesIn(api::namespace));

This would simply be emitted as ::class, which already does everything we want; however, using api::class in this place doesn't convey the intent correctly!

thekid commented 8 months ago

This would break class constants named namespace:

$ xp -w 'class T { const namespace = "ns"; } return T::namespace'
ns
thekid commented 8 months ago

Implementation:

diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php
index fe08e57..6342402 100755
--- a/src/main/php/lang/ast/emit/PHP.class.php
+++ b/src/main/php/lang/ast/emit/PHP.class.php
@@ -1086,17 +1086,17 @@ abstract class PHP extends Emitter {
       $result->out->write("{$scope->type}::");
     }

-    // Rewrite T::member to T::$member for XP enums
-    if (
-      $scope->member instanceof Literal &&
-      is_string($scope->type) &&
-      'class' !== $scope->member->expression &&
-      $result->codegen->lookup($scope->type)->rewriteEnumCase($scope->member->expression)
-    ) {
-      $result->out->write('$'.$scope->member->expression);
-    } else {
-      $this->emitOne($result, $scope->member);
+    // Rewrite ::class and ::namespace to namespace resolution; T::member to T::$member for XP enums
+    if ($scope->member instanceof Literal) {
+      if ('namespace' === $scope->member->expression || 'class' === $scope->member->expression) {
+        $result->out->write('class');
+        return;
+      } else if (is_string($scope->type) && $result->codegen->lookup($scope->type)->rewriteEnumCase($scope->member->expression)) {
+        $result->out->write('$'.$scope->member->expression);
+        return;
+      }
     }
+    $this->emitOne($result, $scope->member);
   }

   protected function emitInstance($result, $instance) {
diff --git a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php
index 7fe7dcc..8db6133 100755
--- a/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php
+++ b/src/test/php/lang/ast/unittest/emit/NamespacesTest.class.php
@@ -84,4 +84,14 @@ class NamespacesTest extends EmittingTest {
     }');
     Assert::equals(new Date('1977-12-14'), $r);
   }
+
+  #[Test]
+  public function namespace_resolution() {
+    $r= $this->run('namespace util; class %T {
+      public function run() {
+        return cmd::namespace;
+      }
+    }');
+    Assert::equals('util\\cmd', $r);
+  }
 }
\ No newline at end of file
thekid commented 8 months ago

Someone suggested this to return the namespace name of a class in https://externals.io/message/113269:

use util\cmd\Console;

$ns= Console::namespace; // "util\cmd"

If we introduced this feature as described above, the above would yield "util\cmd\Console". Currently, it yields an error for an undefined class constant.