PHP 8.3 Support: Typed class constants (Part 6)

- https://github.com/apache/netbeans/issues/6701
- https://wiki.php.net/rfc/typed_class_constants
- Fix the formatter (don't add spaces within parens of DNF types)
- Fix the `UnusableTypeHintError`
- Add unit tests
diff --git a/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatToken.java b/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatToken.java
index 6e812f7..727f8f9 100644
--- a/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatToken.java
+++ b/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatToken.java
@@ -126,6 +126,7 @@
         WHITESPACE_WITHIN_ATTRIBUTE_BRACKETS,
         WHITESPACE_WITHIN_ATTRIBUTE_DECL_PARENS,
         WHITESPACE_WITHIN_TYPE_CAST_PARENS,
+        WHITESPACE_WITHIN_DNF_TYPE_PARENS, // (A&B)|C
         WHITESPACE_WITHIN_DYNAMIC_NAME_BRACES, // {$example}
         WHITESPACE_BEFORE_COMMA,
         WHITESPACE_AFTER_COMMA,
diff --git a/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatVisitor.java b/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatVisitor.java
index eddae94..11058e1 100644
--- a/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatVisitor.java
+++ b/php/php.editor/src/org/netbeans/modules/php/editor/indent/FormatVisitor.java
@@ -1080,13 +1080,14 @@
                 }
             }
             scan(node.getAttributes());
-            while (ts.moveNext() && ts.token().id() != PHPTokenId.PHP_STRING) {
+            while (ts.moveNext() && !isConstTypeToken(ts.token())) {
                 addFormatToken(formatTokens);
             }
+            ts.movePrevious();
             FormatToken lastWhitespace = formatTokens.remove(formatTokens.size() - 1);
             formatTokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_AFTER_MODIFIERS, lastWhitespace.getOffset(), lastWhitespace.getOldText()));
-            addFormatToken(formatTokens);
             formatTokens.add(new FormatToken.IndentToken(node.getStartOffset(), options.continualIndentSize));
+            scan(node.getConstType());
             scan(node.getNames());
             if (node.getNames().size() == 1) {
                 while (ts.moveNext()
@@ -2499,6 +2500,8 @@
     @Override
     public void visit(UnionType node) {
         processUnionOrIntersectionType(node.getTypes());
+        // add ")" if it exists e.g. (A&B)|(B&C)
+        addAllUntilOffset(node.getEndOffset());
     }
 
     @Override
@@ -2729,6 +2732,9 @@
                         tokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_BEFORE_ARRAY_DECL_PAREN, ts.offset()));
                         tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
                         tokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_AFTER_ARRAY_DECL_LEFT_PAREN, ts.offset() + ts.token().length()));
+                    } else if (parent instanceof UnionType) {
+                        tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
+                        tokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_WITHIN_DNF_TYPE_PARENS, ts.offset() + ts.token().length()));
                     } else {
                         tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
                     }
@@ -2767,6 +2773,9 @@
                     } else if (parent instanceof ArrayCreation) {
                         tokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_BEFORE_ARRAY_DECL_RIGHT_PAREN, ts.offset()));
                         tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
+                    } else if (parent instanceof UnionType) {
+                        tokens.add(new FormatToken(FormatToken.Kind.WHITESPACE_WITHIN_DNF_TYPE_PARENS, ts.offset()));
+                        tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
                     } else {
                         tokens.add(new FormatToken(FormatToken.Kind.TEXT, ts.offset(), ts.token().text().toString()));
                     }
@@ -3349,7 +3358,11 @@
 
     private boolean isFieldTypeOrVariableToken(Token<PHPTokenId> token) {
         return PHPTokenId.PHP_VARIABLE == token.id()
-                || PHPTokenId.PHP_STRING == token.id()
+                || isConstTypeToken(token);
+    }
+
+    private boolean isConstTypeToken(Token<PHPTokenId> token) {
+        return PHPTokenId.PHP_STRING == token.id()
                 || PHPTokenId.PHP_ARRAY == token.id()
                 || PHPTokenId.PHP_ITERABLE == token.id()
                 || PHPTokenId.PHP_PARENT == token.id()
@@ -3362,9 +3375,11 @@
                 || PHPTokenId.PHP_NULL == token.id()
                 || PHPTokenId.PHP_FALSE == token.id()
                 || PHPTokenId.PHP_NS_SEPARATOR == token.id() // \
+                || (PHPTokenId.PHP_TOKEN == token.id() && TokenUtilities.textEquals(token.text(), "(")) // NOI18N
                 || (PHPTokenId.PHP_TOKEN == token.id() && TokenUtilities.textEquals(token.text(), "?")) // NOI18N
                 || PHPTokenId.PHP_TYPE_VOID == token.id() // not supported type but just check it
                 || PHPTokenId.PHP_CALLABLE == token.id() // not supported type but just check it
+                || PHPTokenId.PHP_TYPE_NEVER == token.id() // not supported type but just check it
                 ;
     }
 
diff --git a/php/php.editor/src/org/netbeans/modules/php/editor/indent/TokenFormatter.java b/php/php.editor/src/org/netbeans/modules/php/editor/indent/TokenFormatter.java
index 3082147..4764316 100644
--- a/php/php.editor/src/org/netbeans/modules/php/editor/indent/TokenFormatter.java
+++ b/php/php.editor/src/org/netbeans/modules/php/editor/indent/TokenFormatter.java
@@ -1555,6 +1555,10 @@
                                     case WHITESPACE_WITHIN_TYPE_CAST_PARENS:
                                         countSpaces = docOptions.spaceWithinTypeCastParens ? 1 : 0;
                                         break;
+                                    case WHITESPACE_WITHIN_DNF_TYPE_PARENS:
+                                        // change here if we add the option for it
+                                        countSpaces = 0;
+                                        break;
                                     case WHITESPACE_WITHIN_DYNAMIC_NAME_BRACES:
                                         // change here if we add the option for it
                                         countSpaces = 0;
diff --git a/php/php.editor/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintError.java b/php/php.editor/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintError.java
index 227b7ef..a1948e2 100644
--- a/php/php.editor/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintError.java
+++ b/php/php.editor/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintError.java
@@ -40,6 +40,7 @@
 import org.netbeans.modules.php.editor.parser.PHPParseResult;
 import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
 import org.netbeans.modules.php.editor.parser.astnodes.ArrowFunctionDeclaration;
+import org.netbeans.modules.php.editor.parser.astnodes.ConstantDeclaration;
 import org.netbeans.modules.php.editor.parser.astnodes.Expression;
 import org.netbeans.modules.php.editor.parser.astnodes.FieldsDeclaration;
 import org.netbeans.modules.php.editor.parser.astnodes.FormalParameter;
@@ -128,7 +129,19 @@
             }
             Expression fieldType = node.getFieldType();
             if (fieldType != null) {
-                checkFieldType(fieldType, false);
+                checkFieldAndConstType(fieldType, false);
+            }
+            super.visit(node);
+        }
+
+        @Override
+        public void visit(ConstantDeclaration node) {
+            if (CancelSupport.getDefault().isCancelled()) {
+                return;
+            }
+            Expression constType = node.getConstType();
+            if (constType != null) {
+                checkFieldAndConstType(constType, false);
             }
             super.visit(node);
         }
@@ -239,11 +252,11 @@
             super.visit(nullableType);
         }
 
-        private void checkFieldType(@NullAllowed Expression fieldType, boolean isInUnionType) {
+        private void checkFieldAndConstType(@NullAllowed Expression declaredType, boolean isInUnionType) {
             // unusable types: void and callable PHP 7.4
-            Expression type = fieldType;
-            if (fieldType instanceof NullableType) {
-                type = ((NullableType) fieldType).getType();
+            Expression type = declaredType;
+            if (declaredType instanceof NullableType) {
+                type = ((NullableType) declaredType).getType();
             }
             if (type == null) {
                 return;
@@ -262,7 +275,7 @@
                     checkTrueAndFalseAndNullTypes((NamespaceName) type);
                 }
             } else if (type instanceof UnionType) {
-                ((UnionType) type).getTypes().forEach(unionType -> checkFieldType(unionType, true));
+                ((UnionType) type).getTypes().forEach(unionType -> checkFieldAndConstType(unionType, true));
             }
         }
 
diff --git a/php/php.editor/test/unit/data/testfiles/formatting/php82/dnfTypes_01.php.formatted b/php/php.editor/test/unit/data/testfiles/formatting/php82/dnfTypes_01.php.formatted
index 933d833..d7bd04d 100644
--- a/php/php.editor/test/unit/data/testfiles/formatting/php82/dnfTypes_01.php.formatted
+++ b/php/php.editor/test/unit/data/testfiles/formatting/php82/dnfTypes_01.php.formatted
@@ -69,7 +69,7 @@
     private (ClassX&ClassZ)|(ClassY&ClassZ)|ClassX $privateYField;
     protected ClassX|(ClassY&ClassZ)|ClassY $protectedYField;
     public static (ClassY&ClassZ)|ClassX $publicStaticYField;
-    private static ( ClassY&ClassZ)|ClassY $privateStaticYField;
+    private static (ClassY&ClassZ)|ClassY $privateStaticYField;
     protected static (ClassY&ClassZ)|ClassX|ClassY $protectedStaticYField;
 
     public function publicYMethod((ClassY&ClassZ)|ClassX $param1, (ClassY&ClassZ)|ClassX|(ClassX&ClassZ) $param2): ClassX|(ClassY&ClassZ) {
diff --git a/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php
new file mode 100644
index 0000000..cf366b2
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php
@@ -0,0 +1,81 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class A implements Stringable {
+    public function __toString() {
+        return static::class;
+    }
+}
+
+class B extends A {}
+class C extends A {}
+
+class ClassTest {
+public const    WITHOUT_TYPE = 1;
+public const ?int NULLABLE = 1;
+          private const A | B UNION = D_A;
+protected        const       A  &  B INTERSECTION = D_B;
+public const (A   &B   )  |  C        DNF = D_C;
+public const string STRING = 'a';
+public const int INT = 1;
+public          const float FLOAT = 1.5;
+public const       bool BOOL =        true;
+public const          array ARRAY =             ['t', 'e', 's', 't'];
+public const iterable          ITERABLE = [         'a', 'b', 'c'];
+public          const mixed MIXED = 1;
+public const             object OBJECT = D_A;
+public     const string  |   array UNION2 = 'a'    ,     UNION3 = ['a'];
+#[Attr]
+public const int    |    null    UNION4 =    null;
+}
+
+interface InterfaceTest {
+               const string STRING = "string";
+public          const      ?     int NULLABLE = 1;
+public const A|B UNION = D_A;
+public const                      A&B INTERSECTION = D_B;
+public const ( A &  B  )            |C DNF = D_C;
+}
+
+trait TraitTest {
+const string STRING = "string";
+public const ?int NULLABLE = 1;
+private       const  A|B UNION = D_A;
+protected const  A&B INTERSECTION = D_B;
+public const             (A&B)|C DNF = D_C;
+}
+
+enum EnumTest {
+    const string STRING =                "string";
+    public const ?  int NULLABLE =      1;
+    private const A|  B UNION=D_A;
+    protected const    A&B INTERSECTION=D_B;
+    public const (A & B)  |  (A&C   ) DNF=D_C;
+    public const     static A=EnumTest    ::     Test;
+
+    case Test;
+}
+
+define("D_A", new A());
+define("D_B", new B());
+define("D_C", new C());
+
+echo ClassTest::DNF . PHP_EOL;
+var_dump(ClassTest::DNF);
diff --git a/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php.testTypedClassConstants_01.formatted b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php.testTypedClassConstants_01.formatted
new file mode 100644
index 0000000..c6a7b4a
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_01.php.testTypedClassConstants_01.formatted
@@ -0,0 +1,93 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class A implements Stringable {
+
+    public function __toString() {
+        return static::class;
+    }
+}
+
+class B extends A {
+    
+}
+
+class C extends A {
+    
+}
+
+class ClassTest {
+
+    public const WITHOUT_TYPE = 1;
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+    public const string STRING = 'a';
+    public const int INT = 1;
+    public const float FLOAT = 1.5;
+    public const bool BOOL = true;
+    public const array ARRAY = ['t', 'e', 's', 't'];
+    public const iterable ITERABLE = ['a', 'b', 'c'];
+    public const mixed MIXED = 1;
+    public const object OBJECT = D_A;
+    public const string|array UNION2 = 'a', UNION3 = ['a'];
+
+    #[Attr]
+    public const int|null UNION4 = null;
+}
+
+interface InterfaceTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    public const A|B UNION = D_A;
+    public const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+trait TraitTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+enum EnumTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|(A&C) DNF = D_C;
+    public const static A = EnumTest::Test;
+
+    case Test;
+}
+
+define("D_A", new A());
+define("D_B", new B());
+define("D_C", new C());
+
+echo ClassTest::DNF . PHP_EOL;
+var_dump(ClassTest::DNF);
diff --git a/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php
new file mode 100644
index 0000000..c6a7b4a
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php
@@ -0,0 +1,93 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class A implements Stringable {
+
+    public function __toString() {
+        return static::class;
+    }
+}
+
+class B extends A {
+    
+}
+
+class C extends A {
+    
+}
+
+class ClassTest {
+
+    public const WITHOUT_TYPE = 1;
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+    public const string STRING = 'a';
+    public const int INT = 1;
+    public const float FLOAT = 1.5;
+    public const bool BOOL = true;
+    public const array ARRAY = ['t', 'e', 's', 't'];
+    public const iterable ITERABLE = ['a', 'b', 'c'];
+    public const mixed MIXED = 1;
+    public const object OBJECT = D_A;
+    public const string|array UNION2 = 'a', UNION3 = ['a'];
+
+    #[Attr]
+    public const int|null UNION4 = null;
+}
+
+interface InterfaceTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    public const A|B UNION = D_A;
+    public const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+trait TraitTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+enum EnumTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|(A&C) DNF = D_C;
+    public const static A = EnumTest::Test;
+
+    case Test;
+}
+
+define("D_A", new A());
+define("D_B", new B());
+define("D_C", new C());
+
+echo ClassTest::DNF . PHP_EOL;
+var_dump(ClassTest::DNF);
diff --git a/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php.testTypedClassConstants_02.formatted b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php.testTypedClassConstants_02.formatted
new file mode 100644
index 0000000..c6a7b4a
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/formatting/php83/typedClassConstants_02.php.testTypedClassConstants_02.formatted
@@ -0,0 +1,93 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class A implements Stringable {
+
+    public function __toString() {
+        return static::class;
+    }
+}
+
+class B extends A {
+    
+}
+
+class C extends A {
+    
+}
+
+class ClassTest {
+
+    public const WITHOUT_TYPE = 1;
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+    public const string STRING = 'a';
+    public const int INT = 1;
+    public const float FLOAT = 1.5;
+    public const bool BOOL = true;
+    public const array ARRAY = ['t', 'e', 's', 't'];
+    public const iterable ITERABLE = ['a', 'b', 'c'];
+    public const mixed MIXED = 1;
+    public const object OBJECT = D_A;
+    public const string|array UNION2 = 'a', UNION3 = ['a'];
+
+    #[Attr]
+    public const int|null UNION4 = null;
+}
+
+interface InterfaceTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    public const A|B UNION = D_A;
+    public const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+trait TraitTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|C DNF = D_C;
+}
+
+enum EnumTest {
+
+    const string STRING = "string";
+    public const ?int NULLABLE = 1;
+    private const A|B UNION = D_A;
+    protected const A&B INTERSECTION = D_B;
+    public const (A&B)|(A&C) DNF = D_C;
+    public const static A = EnumTest::Test;
+
+    case Test;
+}
+
+define("D_A", new A());
+define("D_B", new B());
+define("D_C", new C());
+
+echo ClassTest::DNF . PHP_EOL;
+var_dump(ClassTest::DNF);
diff --git a/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php b/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php
new file mode 100644
index 0000000..b9691a3
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php
@@ -0,0 +1,99 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class ClassTest {
+    const callable TEST1 = T::TEST;
+    const void TEST2 = T::TEST;
+    const never TEST3 = T::TEST;
+    const int|callable callable = T::TEST;
+    const false false = T::TEST;
+    const null null = T::TEST;
+    const true true = T::TEST;
+    const bool|false boolFalse = T::TEST;
+    const true|bool trueBool = T::TEST;
+    const bool|bool duplicatedBool = T::TEST;
+    const true|false bothTrueAndFalse = T::TEST;
+    const int|false|true bothTrueAndFalse2 = T::TEST;
+    const int|INT duplicatedInt = T::TEST;
+    const iterable|array iterable1 = T::TEST;
+    const iterable|Traversable iterable2 = T::TEST;
+    const iterable|array|Traversable iterable3 = T::TEST;
+    const null|false nullFalse = T::TEST; // PHP 8.2: OK
+}
+
+interface InterfaceTest {
+    public const callable TEST1 = T::TEST;
+    public const void TEST2 = T::TEST;
+    public const never TEST3 = T::TEST;
+    public const int|callable callable = T::TEST;
+    public const false false = T::TEST;
+    public const null null = T::TEST;
+    public const true true = T::TEST;
+    public const bool|false boolFalse = T::TEST;
+    public const true|bool trueBool = T::TEST;
+    public const bool|bool duplicatedBool = T::TEST;
+    public const true|false bothTrueAndFalse = T::TEST;
+    public const int|false|true bothTrueAndFalse2 = T::TEST;
+    public const int|INT duplicatedInt = T::TEST;
+    public const iterable|array iterable1 = T::TEST;
+    public const iterable|Traversable iterable2 = T::TEST;
+    public const iterable|array|Traversable iterable3 = T::TEST;
+    public const null|false nullFalse = T::TEST; // PHP 8.2: OK
+}
+
+trait TraitTest {
+    private const callable TEST1 = T::TEST;
+    private const void TEST2 = T::TEST;
+    private const never TEST3 = T::TEST;
+    private const int|callable callable = T::TEST;
+    private const false false = T::TEST;
+    private const null null = T::TEST;
+    private const true true = T::TEST;
+    private const bool|false boolFalse = T::TEST;
+    private const true|bool trueBool = T::TEST;
+    private const bool|bool duplicatedBool = T::TEST;
+    private const true|false bothTrueAndFalse = T::TEST;
+    private const int|false|true bothTrueAndFalse2 = T::TEST;
+    private const int|INT duplicatedInt = T::TEST;
+    private const iterable|array iterable1 = T::TEST;
+    private const iterable|Traversable iterable2 = T::TEST;
+    private const iterable|array|Traversable iterable3 = T::TEST;
+    private const null|false nullFalse = T::TEST; // PHP 8.2: OK
+}
+
+enum EnumTest {
+    protected const callable TEST1 = T::TEST;
+    protected const void TEST2 = T::TEST;
+    protected const never TEST3 = T::TEST;
+    protected const int|callable callable = T::TEST;
+    protected const false false = T::TEST;
+    protected const null null = T::TEST;
+    protected const true true = T::TEST;
+    protected const bool|false boolFalse = T::TEST;
+    protected const true|bool trueBool = T::TEST;
+    protected const bool|bool duplicatedBool = T::TEST;
+    protected const true|false bothTrueAndFalse = T::TEST;
+    protected const int|false|true bothTrueAndFalse2 = T::TEST;
+    protected const int|INT duplicatedInt = T::TEST;
+    protected const iterable|array iterable1 = T::TEST;
+    protected const iterable|Traversable iterable2 = T::TEST;
+    protected const iterable|array|Traversable iterable3 = T::TEST;
+    protected const null|false nullFalse = T::TEST; // PHP 8.2: OK
+}
diff --git a/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php.testConstantTypes_01.hints b/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php.testConstantTypes_01.hints
new file mode 100644
index 0000000..e1f1025
--- /dev/null
+++ b/php/php.editor/test/unit/data/testfiles/verification/UnusableTypesHintError/testConstantTypes_01.php.testConstantTypes_01.hints
@@ -0,0 +1,168 @@
+    const callable TEST1 = T::TEST;
+          --------
+HINT:"callable" cannot be used as a property type.
+    const void TEST2 = T::TEST;
+          ----
+HINT:"void" cannot be used as a property type.
+    const never TEST3 = T::TEST;
+          -----
+HINT:"never" cannot be used as a property type.
+    const int|callable callable = T::TEST;
+              --------
+HINT:"callable" cannot be used as a property type.
+    const bool|false boolFalse = T::TEST;
+               -----
+HINT:Type "false" is duplicated.
+    const true|bool trueBool = T::TEST;
+               ----
+HINT:Type "bool" is duplicated.
+    const bool|bool duplicatedBool = T::TEST;
+               ----
+HINT:Type "bool" is duplicated.
+    const true|false bothTrueAndFalse = T::TEST;
+               -----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    const int|false|true bothTrueAndFalse2 = T::TEST;
+                    ----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    const int|INT duplicatedInt = T::TEST;
+              ---
+HINT:Type "INT" is duplicated.
+    const iterable|array iterable1 = T::TEST;
+          --------------
+HINT:Redundant combination: "iterable|array" contains both "iterable" and "array".
+    const iterable|Traversable iterable2 = T::TEST;
+          --------------------
+HINT:Redundant combination: "iterable|Traversable" contains both "iterable" and "Traversable".
+    const iterable|array|Traversable iterable3 = T::TEST;
+          --------------------------
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "Traversable".
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "array".
+    public const callable TEST1 = T::TEST;
+                 --------
+HINT:"callable" cannot be used as a property type.
+    public const void TEST2 = T::TEST;
+                 ----
+HINT:"void" cannot be used as a property type.
+    public const never TEST3 = T::TEST;
+                 -----
+HINT:"never" cannot be used as a property type.
+    public const int|callable callable = T::TEST;
+                     --------
+HINT:"callable" cannot be used as a property type.
+    public const bool|false boolFalse = T::TEST;
+                      -----
+HINT:Type "false" is duplicated.
+    public const true|bool trueBool = T::TEST;
+                      ----
+HINT:Type "bool" is duplicated.
+    public const bool|bool duplicatedBool = T::TEST;
+                      ----
+HINT:Type "bool" is duplicated.
+    public const true|false bothTrueAndFalse = T::TEST;
+                      -----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    public const int|false|true bothTrueAndFalse2 = T::TEST;
+                           ----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    public const int|INT duplicatedInt = T::TEST;
+                     ---
+HINT:Type "INT" is duplicated.
+    public const iterable|array iterable1 = T::TEST;
+                 --------------
+HINT:Redundant combination: "iterable|array" contains both "iterable" and "array".
+    public const iterable|Traversable iterable2 = T::TEST;
+                 --------------------
+HINT:Redundant combination: "iterable|Traversable" contains both "iterable" and "Traversable".
+    public const iterable|array|Traversable iterable3 = T::TEST;
+                 --------------------------
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "Traversable".
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "array".
+    private const callable TEST1 = T::TEST;
+                  --------
+HINT:"callable" cannot be used as a property type.
+    private const void TEST2 = T::TEST;
+                  ----
+HINT:"void" cannot be used as a property type.
+    private const never TEST3 = T::TEST;
+                  -----
+HINT:"never" cannot be used as a property type.
+    private const int|callable callable = T::TEST;
+                      --------
+HINT:"callable" cannot be used as a property type.
+    private const bool|false boolFalse = T::TEST;
+                       -----
+HINT:Type "false" is duplicated.
+    private const true|bool trueBool = T::TEST;
+                       ----
+HINT:Type "bool" is duplicated.
+    private const bool|bool duplicatedBool = T::TEST;
+                       ----
+HINT:Type "bool" is duplicated.
+    private const true|false bothTrueAndFalse = T::TEST;
+                       -----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    private const int|false|true bothTrueAndFalse2 = T::TEST;
+                            ----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    private const int|INT duplicatedInt = T::TEST;
+                      ---
+HINT:Type "INT" is duplicated.
+    private const iterable|array iterable1 = T::TEST;
+                  --------------
+HINT:Redundant combination: "iterable|array" contains both "iterable" and "array".
+    private const iterable|Traversable iterable2 = T::TEST;
+                  --------------------
+HINT:Redundant combination: "iterable|Traversable" contains both "iterable" and "Traversable".
+    private const iterable|array|Traversable iterable3 = T::TEST;
+                  --------------------------
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "Traversable".
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "array".
+    protected const callable TEST1 = T::TEST;
+                    --------
+HINT:"callable" cannot be used as a property type.
+    protected const void TEST2 = T::TEST;
+                    ----
+HINT:"void" cannot be used as a property type.
+    protected const never TEST3 = T::TEST;
+                    -----
+HINT:"never" cannot be used as a property type.
+    protected const int|callable callable = T::TEST;
+                        --------
+HINT:"callable" cannot be used as a property type.
+    protected const bool|false boolFalse = T::TEST;
+                         -----
+HINT:Type "false" is duplicated.
+    protected const true|bool trueBool = T::TEST;
+                         ----
+HINT:Type "bool" is duplicated.
+    protected const bool|bool duplicatedBool = T::TEST;
+                         ----
+HINT:Type "bool" is duplicated.
+    protected const true|false bothTrueAndFalse = T::TEST;
+                         -----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    protected const int|false|true bothTrueAndFalse2 = T::TEST;
+                              ----
+HINT:Contains both "true" and "false", "bool" should be used.
+HINT:Contains both "true" and "false", "bool" should be used.
+    protected const int|INT duplicatedInt = T::TEST;
+                        ---
+HINT:Type "INT" is duplicated.
+    protected const iterable|array iterable1 = T::TEST;
+                    --------------
+HINT:Redundant combination: "iterable|array" contains both "iterable" and "array".
+    protected const iterable|Traversable iterable2 = T::TEST;
+                    --------------------
+HINT:Redundant combination: "iterable|Traversable" contains both "iterable" and "Traversable".
+    protected const iterable|array|Traversable iterable3 = T::TEST;
+                    --------------------------
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "Traversable".
+HINT:Redundant combination: "iterable|array|Traversable" contains both "iterable" and "array".
diff --git a/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/indent/PHPFormatterTest.java b/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/indent/PHPFormatterTest.java
index 130ff79..ddfbd74 100644
--- a/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/indent/PHPFormatterTest.java
+++ b/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/indent/PHPFormatterTest.java
@@ -1128,4 +1128,15 @@
         options.put(FmtOptions.SPACE_AROUND_SCOPE_RESOLUTION_OPS, true);
         reformatFileContents("testfiles/formatting/php83/dynamicClassConstantFetch_02.php", options, false, true);
     }
+
+    public void testTypedClassConstants_01() throws Exception {
+        HashMap<String, Object> options = new HashMap<>(FmtOptions.getDefaults());
+        reformatFileContents("testfiles/formatting/php83/typedClassConstants_01.php", options, false, true);
+    }
+
+    public void testTypedClassConstants_02() throws Exception {
+        HashMap<String, Object> options = new HashMap<>(FmtOptions.getDefaults());
+        reformatFileContents("testfiles/formatting/php83/typedClassConstants_02.php", options, false, true);
+    }
+
 }
diff --git a/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintErrorTest.java b/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintErrorTest.java
index 81b375a..0a48833 100644
--- a/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintErrorTest.java
+++ b/php/php.editor/test/unit/src/org/netbeans/modules/php/editor/verification/UnusableTypesHintErrorTest.java
@@ -75,6 +75,10 @@
         checkHints(new UnusableTypesHintError(), "testDuplicateTypes_01.php");
     }
 
+    public void testConstantTypes_01() throws Exception {
+        checkHints(new UnusableTypesHintError(), "testConstantTypes_01.php");
+    }
+
     @Override
     protected String getTestDirectory() {
         return TEST_DIRECTORY + "UnusableTypesHintError/";