├── .github ├── FUNDING.yml └── workflows │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── Justfile ├── LICENSE ├── README.md ├── art └── hero.png ├── composer.json ├── composer.lock ├── crates ├── ast │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── meta │ │ └── ast.yaml │ └── src │ │ ├── array.rs │ │ ├── backed_enum_type.rs │ │ ├── comments.rs │ │ ├── data_type.rs │ │ ├── docblock.rs │ │ ├── generated.rs │ │ ├── id.rs │ │ ├── identifiers.rs │ │ ├── lib.rs │ │ ├── literals.rs │ │ ├── modifiers.rs │ │ ├── name.rs │ │ ├── node.rs │ │ ├── operators.rs │ │ ├── properties.rs │ │ ├── spanned.rs │ │ ├── utils.rs │ │ ├── variables.rs │ │ ├── visibility.rs │ │ └── visitor │ │ ├── immutable.rs │ │ ├── mod.rs │ │ ├── mutable.rs │ │ ├── node.rs │ │ ├── walk.rs │ │ └── walk_mut.rs ├── bytestring │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── bytestr.rs │ │ ├── bytestring.rs │ │ └── lib.rs ├── diagnostics │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── severity.rs ├── index │ ├── Cargo.toml │ ├── src │ │ ├── entities │ │ │ ├── class.rs │ │ │ ├── function.rs │ │ │ ├── method.rs │ │ │ ├── mod.rs │ │ │ └── parameters.rs │ │ ├── file.rs │ │ ├── indexer.rs │ │ ├── lib.rs │ │ ├── location.rs │ │ └── reflection │ │ │ ├── class.rs │ │ │ ├── function.rs │ │ │ ├── method.rs │ │ │ ├── mod.rs │ │ │ ├── parameters.rs │ │ │ └── type.rs │ └── tests │ │ ├── fixtures │ │ └── functions.php │ │ └── index.rs ├── inference │ ├── Cargo.toml │ └── src │ │ ├── engine.rs │ │ ├── lib.rs │ │ └── map.rs ├── lexer │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── diagnostics.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── macros.rs │ │ └── source.rs ├── node-finder │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── parser │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── internal │ │ │ ├── arrays.rs │ │ │ ├── attributes.rs │ │ │ ├── blocks.rs │ │ │ ├── classes.rs │ │ │ ├── comments.rs │ │ │ ├── constants.rs │ │ │ ├── control_flow.rs │ │ │ ├── data_type.rs │ │ │ ├── diagnostics.rs │ │ │ ├── docblock.rs │ │ │ ├── enums.rs │ │ │ ├── expressions.rs │ │ │ ├── functions.rs │ │ │ ├── goto.rs │ │ │ ├── identifiers.rs │ │ │ ├── imports.rs │ │ │ ├── interfaces.rs │ │ │ ├── literals.rs │ │ │ ├── loops.rs │ │ │ ├── mod.rs │ │ │ ├── modifiers.rs │ │ │ ├── names.rs │ │ │ ├── namespaces.rs │ │ │ ├── parameters.rs │ │ │ ├── precedences.rs │ │ │ ├── properties.rs │ │ │ ├── statement.rs │ │ │ ├── strings.rs │ │ │ ├── traits.rs │ │ │ ├── try_block.rs │ │ │ ├── uses.rs │ │ │ ├── utils.rs │ │ │ └── variables.rs │ │ ├── lib.rs │ │ └── macros.rs │ └── tests │ │ ├── __snapshots__ │ │ ├── abstract_class.snap │ │ ├── add.snap │ │ ├── add_assign.snap │ │ ├── and.snap │ │ ├── array_trailing_comma.snap │ │ ├── assign.snap │ │ ├── asymmetric_visibility.snap │ │ ├── asymmetric_visibility_promoted_property.snap │ │ ├── attribute.snap │ │ ├── attribute_use.snap │ │ ├── attribute_use_aliased.snap │ │ ├── backed_enum_int.snap │ │ ├── backed_enum_string.snap │ │ ├── binary_op_multi_right_hand_assignment.snap │ │ ├── binary_op_right_hand_assignment.snap │ │ ├── bitwise_and.snap │ │ ├── bitwise_and_assign.snap │ │ ├── bitwise_left_shift.snap │ │ ├── bitwise_left_shift_assign.snap │ │ ├── bitwise_not.snap │ │ ├── bitwise_or.snap │ │ ├── bitwise_or_assign.snap │ │ ├── bitwise_right_shift.snap │ │ ├── bitwise_right_shift_assign.snap │ │ ├── bitwise_xor.snap │ │ ├── bitwise_xor_assign.snap │ │ ├── bool.snap │ │ ├── braced_namespace.snap │ │ ├── class_const.snap │ │ ├── class_const_with_attributes.snap │ │ ├── class_extends.snap │ │ ├── class_extends_aliased.snap │ │ ├── class_extends_qualified_aliased.snap │ │ ├── class_implements.snap │ │ ├── class_implements_aliased.snap │ │ ├── class_implements_qualified_aliased.snap │ │ ├── class_in_namespace.snap │ │ ├── class_with_attributes.snap │ │ ├── class_with_constants.snap │ │ ├── class_with_extends.snap │ │ ├── class_with_extends_and_implements.snap │ │ ├── class_with_implements.snap │ │ ├── class_with_methods.snap │ │ ├── class_with_properties.snap │ │ ├── class_with_static_methods.snap │ │ ├── class_with_static_properties.snap │ │ ├── class_with_traits.snap │ │ ├── class_with_traits_and_alias.snap │ │ ├── class_with_traits_and_insteadof.snap │ │ ├── class_with_traits_and_visibility.snap │ │ ├── clone_function_call.snap │ │ ├── clone_function_call_args.snap │ │ ├── clone_property_fetch.snap │ │ ├── clone_var.snap │ │ ├── coalesce_assign.snap │ │ ├── concat_assign.snap │ │ ├── constant.snap │ │ ├── constant_namespaced.snap │ │ ├── constant_unnamespaced.snap │ │ ├── constant_use.snap │ │ ├── div.snap │ │ ├── div_assign.snap │ │ ├── do_while_statement.snap │ │ ├── docblock_array_shape.snap │ │ ├── docblock_array_shape_ident_keyed.snap │ │ ├── docblock_array_shape_int_keyed.snap │ │ ├── docblock_array_shape_keyless.snap │ │ ├── docblock_array_shape_mixed_keyed.snap │ │ ├── docblock_array_shape_string_keyed.snap │ │ ├── docblock_array_shape_unsealed.snap │ │ ├── docblock_array_shape_unsealed_list.snap │ │ ├── docblock_array_shape_variadic.snap │ │ ├── docblock_empty.snap │ │ ├── docblock_empty_multiline.snap │ │ ├── docblock_method_tag_static_return_type_parameters.snap │ │ ├── docblock_method_tag_templates.snap │ │ ├── docblock_param_dnf_type.snap │ │ ├── docblock_param_empty.snap │ │ ├── docblock_param_intersection_type.snap │ │ ├── docblock_param_intersection_typed_array.snap │ │ ├── docblock_param_multiple_generic_type.snap │ │ ├── docblock_param_nested_generic_type.snap │ │ ├── docblock_param_nested_typed_array.snap │ │ ├── docblock_param_nullable_type.snap │ │ ├── docblock_param_simple_generic_type.snap │ │ ├── docblock_param_simple_typed_array.snap │ │ ├── docblock_param_type.snap │ │ ├── docblock_param_type_variable.snap │ │ ├── docblock_param_type_variable_description.snap │ │ ├── docblock_param_union_type.snap │ │ ├── docblock_param_union_typed_array.snap │ │ ├── docblock_param_unparenthesized_union_typed_array.snap │ │ ├── docblock_param_variable.snap │ │ ├── docblock_param_variable_description.snap │ │ ├── docblock_property_read_tag.snap │ │ ├── docblock_property_tag.snap │ │ ├── docblock_property_write_tag.snap │ │ ├── docblock_template_tag.snap │ │ ├── docblock_template_tag_bound.snap │ │ ├── docblock_template_tag_default.snap │ │ ├── docblock_template_tag_lower_bound.snap │ │ ├── docblock_text.snap │ │ ├── docblock_text_multiline.snap │ │ ├── docblock_var_empty.snap │ │ ├── docblock_var_type.snap │ │ ├── docblock_var_type_text.snap │ │ ├── docblock_var_type_variable.snap │ │ ├── docblock_var_type_variable_text.snap │ │ ├── dynamic_class_const.snap │ │ ├── echo_missing_semicolon.snap │ │ ├── echo_no_value.snap │ │ ├── echo_single_value_trailing_comma.snap │ │ ├── echo_tag.snap │ │ ├── empty_array.snap │ │ ├── empty_file.snap │ │ ├── empty_heredoc.snap │ │ ├── empty_nowdoc.snap │ │ ├── enum_in_namespace.snap │ │ ├── enum_static.snap │ │ ├── enum_static_use.snap │ │ ├── enum_static_use_alias.snap │ │ ├── enum_with_attributes.snap │ │ ├── enum_with_constants.snap │ │ ├── enum_with_implements.snap │ │ ├── enum_with_methods.snap │ │ ├── equal.snap │ │ ├── exp.snap │ │ ├── exp_assign.snap │ │ ├── final_class.snap │ │ ├── float.snap │ │ ├── for_statement.snap │ │ ├── foreach_statement.snap │ │ ├── foreach_statement_with_key.snap │ │ ├── from_static.snap │ │ ├── from_static_use.snap │ │ ├── from_static_use_alias.snap │ │ ├── fully_qualified_identifier.snap │ │ ├── function_argument_unqualified.snap │ │ ├── function_argument_unqualified_use.snap │ │ ├── function_call_qualified.snap │ │ ├── function_call_unqualified.snap │ │ ├── function_call_use.snap │ │ ├── function_in_namespace.snap │ │ ├── function_with_parameter_types.snap │ │ ├── function_with_return_type.snap │ │ ├── global_namespace.snap │ │ ├── greater_than.snap │ │ ├── greater_than_or_equal.snap │ │ ├── group_use.snap │ │ ├── group_use_multiple_types.snap │ │ ├── heredoc_interpolation.snap │ │ ├── html.snap │ │ ├── identical.snap │ │ ├── if_else_statement.snap │ │ ├── if_elseif_else_statement.snap │ │ ├── if_elseif_else_statement_no_else.snap │ │ ├── if_statement.snap │ │ ├── inline_html_with_php.snap │ │ ├── int.snap │ │ ├── interface_extends.snap │ │ ├── interface_in_namespace.snap │ │ ├── interface_with_constants.snap │ │ ├── interface_with_extends.snap │ │ ├── interface_with_methods.snap │ │ ├── legacy_array.snap │ │ ├── legacy_array_single_item.snap │ │ ├── less_than.snap │ │ ├── less_than_or_equal.snap │ │ ├── logical_and.snap │ │ ├── logical_or.snap │ │ ├── logical_xor.snap │ │ ├── magic_constants.snap │ │ ├── match_expression.snap │ │ ├── match_expression_no_default.snap │ │ ├── method.snap │ │ ├── method_argument_unqualified.snap │ │ ├── method_argument_unqualified_use.snap │ │ ├── method_with_abstract.snap │ │ ├── method_with_attributes.snap │ │ ├── method_with_final.snap │ │ ├── method_with_parameters.snap │ │ ├── method_with_parameters_with_default.snap │ │ ├── method_with_parameters_with_type.snap │ │ ├── method_with_parameters_with_type_and_default.snap │ │ ├── method_with_return_type.snap │ │ ├── method_with_static.snap │ │ ├── method_with_visibility.snap │ │ ├── mod_.snap │ │ ├── mod_assign.snap │ │ ├── more_nested_array.snap │ │ ├── mul.snap │ │ ├── mul_assign.snap │ │ ├── multi_assign.snap │ │ ├── multi_class_const.snap │ │ ├── multi_echo.snap │ │ ├── multi_item_array.snap │ │ ├── multi_op_arithmetic.snap │ │ ├── nested_array.snap │ │ ├── new_class.snap │ │ ├── new_class_aliased.snap │ │ ├── new_class_fqn.snap │ │ ├── new_class_qualified.snap │ │ ├── new_class_qualified_aliased.snap │ │ ├── new_parent.snap │ │ ├── new_self.snap │ │ ├── new_static.snap │ │ ├── not.snap │ │ ├── not_equal.snap │ │ ├── not_identical.snap │ │ ├── null.snap │ │ ├── or.snap │ │ ├── post_dec.snap │ │ ├── post_inc.snap │ │ ├── pre_dec.snap │ │ ├── pre_inc.snap │ │ ├── property_hooks_abstract_get.snap │ │ ├── property_hooks_abstract_set.snap │ │ ├── property_hooks_block_body.snap │ │ ├── property_hooks_empty.snap │ │ ├── property_hooks_expression_body.snap │ │ ├── property_hooks_parameter_list.snap │ │ ├── qualified_identifier.snap │ │ ├── qualified_use.snap │ │ ├── readonly_class.snap │ │ ├── short_tag.snap │ │ ├── simple_class.snap │ │ ├── simple_echo.snap │ │ ├── simple_enum.snap │ │ ├── simple_function.snap │ │ ├── simple_function_call.snap │ │ ├── simple_function_call_args.snap │ │ ├── simple_function_call_args_trailing_comma.snap │ │ ├── simple_heredoc.snap │ │ ├── simple_interface.snap │ │ ├── simple_nowdoc.snap │ │ ├── simple_trait.snap │ │ ├── simple_use.snap │ │ ├── single_item_array.snap │ │ ├── spaceship.snap │ │ ├── string.snap │ │ ├── sub.snap │ │ ├── sub_assign.snap │ │ ├── switch_statement.snap │ │ ├── switch_statement_no_case.snap │ │ ├── tag.snap │ │ ├── trait_in_namespace.snap │ │ ├── trait_with_methods.snap │ │ ├── trait_with_properties.snap │ │ ├── trait_with_trait_use.snap │ │ ├── typed_class_const.snap │ │ ├── unbraced_namespace.snap │ │ ├── unqualified_identifier.snap │ │ ├── use_with_alias.snap │ │ ├── variable.snap │ │ ├── variable_variable.snap │ │ ├── variable_variable_complex.snap │ │ ├── while_statement.snap │ │ ├── xor.snap │ │ ├── yield_empty.snap │ │ ├── yield_from.snap │ │ ├── yield_key_value.snap │ │ └── yield_value.snap │ │ ├── docblocks.rs │ │ ├── fixtures │ │ ├── arithmetic │ │ │ ├── add.php │ │ │ ├── div.php │ │ │ ├── exp.php │ │ │ ├── mod.php │ │ │ ├── mul.php │ │ │ ├── post-dec.php │ │ │ ├── post-inc.php │ │ │ ├── pre-dec.php │ │ │ ├── pre-inc.php │ │ │ └── sub.php │ │ ├── assignments │ │ │ ├── add-assign.php │ │ │ ├── assign.php │ │ │ ├── bitwise-and-assign.php │ │ │ ├── bitwise-left-shift-assign.php │ │ │ ├── bitwise-or-assign.php │ │ │ ├── bitwise-right-shift-assign.php │ │ │ ├── bitwise-xor-assign.php │ │ │ ├── coalesce-assign.php │ │ │ ├── concat-assign.php │ │ │ ├── div-assign.php │ │ │ ├── exp-assign.php │ │ │ ├── mod-assign.php │ │ │ ├── mul-assign.php │ │ │ ├── multi-assign.php │ │ │ └── sub-assign.php │ │ ├── asymmetric-visibility │ │ │ ├── promoted-property.php │ │ │ └── visibility.php │ │ ├── bitwise │ │ │ ├── bitwise-and.php │ │ │ ├── bitwise-left-shift.php │ │ │ ├── bitwise-not.php │ │ │ ├── bitwise-or.php │ │ │ ├── bitwise-right-shift.php │ │ │ └── bitwise-xor.php │ │ ├── class-constants │ │ │ ├── class-const-with-attributes.php │ │ │ ├── class-const.php │ │ │ ├── dynamic-class-const.php │ │ │ ├── multi-class-const.php │ │ │ └── typed-class-const.php │ │ ├── classes │ │ │ ├── abstract-class.php │ │ │ ├── class-with-attributes.php │ │ │ ├── class-with-constants.php │ │ │ ├── class-with-extends-and-implements.php │ │ │ ├── class-with-extends.php │ │ │ ├── class-with-implements.php │ │ │ ├── class-with-methods.php │ │ │ ├── class-with-properties.php │ │ │ ├── class-with-static-methods.php │ │ │ ├── class-with-static-properties.php │ │ │ ├── class-with-traits-and-alias.php │ │ │ ├── class-with-traits-and-insteadof.php │ │ │ ├── class-with-traits-and-visibility.php │ │ │ ├── class-with-traits.php │ │ │ ├── final-class.php │ │ │ ├── readonly-class.php │ │ │ └── simple-class.php │ │ ├── clone │ │ │ ├── clone-function-call-args.php │ │ │ ├── clone-function-call.php │ │ │ ├── clone-property-fetch.php │ │ │ └── clone-var.php │ │ ├── comparison │ │ │ ├── equal.php │ │ │ ├── greater-than-or-equal.php │ │ │ ├── greater-than.php │ │ │ ├── identical.php │ │ │ ├── less-than-or-equal.php │ │ │ ├── less-than.php │ │ │ ├── not-equal.php │ │ │ ├── not-identical.php │ │ │ └── spaceship.php │ │ ├── constants │ │ │ ├── constant.php │ │ │ └── magic-constants.php │ │ ├── control │ │ │ ├── do-while-statement.php │ │ │ ├── for-statement.php │ │ │ ├── foreach-statement-with-key.php │ │ │ ├── foreach-statement.php │ │ │ ├── if-else-statement.php │ │ │ ├── if-elseif-else-statement.php │ │ │ ├── if-elseif-statement-no-else.php │ │ │ ├── if-statement.php │ │ │ ├── match-expression-no-default.php │ │ │ ├── match-expression.php │ │ │ ├── switch-statement-no-case.php │ │ │ ├── switch-statement.php │ │ │ └── while-statement.php │ │ ├── docblocks │ │ │ ├── array-shape-ident-keyed.php │ │ │ ├── array-shape-int-keyed.php │ │ │ ├── array-shape-keyless.php │ │ │ ├── array-shape-mixed-keyed.php │ │ │ ├── array-shape-string-keyed.php │ │ │ ├── array-shape-unsealed-list.php │ │ │ ├── array-shape-unsealed.php │ │ │ ├── array-shape-variadic.php │ │ │ ├── array-shape.php │ │ │ ├── empty-multiline.php │ │ │ ├── empty.php │ │ │ ├── extends.php │ │ │ ├── method-tag-static-return-type-parameters.php │ │ │ ├── method-tag-templates.php │ │ │ ├── param-dnf-type.php │ │ │ ├── param-empty.php │ │ │ ├── param-intersection-type.php │ │ │ ├── param-intersection-typed-array.php │ │ │ ├── param-multiple-generic-type.php │ │ │ ├── param-nested-generic-type.php │ │ │ ├── param-nested-typed-array.php │ │ │ ├── param-nullable-type.php │ │ │ ├── param-simple-generic-type.php │ │ │ ├── param-simple-typed-array.php │ │ │ ├── param-type-variable-description.php │ │ │ ├── param-type-variable.php │ │ │ ├── param-type.php │ │ │ ├── param-union-type.php │ │ │ ├── param-union-typed-array.php │ │ │ ├── param-unparenthesized-union-typed-array.php │ │ │ ├── param-variable-description.php │ │ │ ├── param-variable.php │ │ │ ├── property-read-tag.php │ │ │ ├── property-tag.php │ │ │ ├── property-write-tag.php │ │ │ ├── template-tag-bound.php │ │ │ ├── template-tag-default.php │ │ │ ├── template-tag-lower-bound.php │ │ │ ├── template-tag.php │ │ │ ├── text-multiline.php │ │ │ ├── text.php │ │ │ ├── var-empty.php │ │ │ ├── var-type-text.php │ │ │ ├── var-type-variable-text.php │ │ │ ├── var-type-variable.php │ │ │ └── var-type.php │ │ ├── docstrings │ │ │ ├── empty-heredoc.php │ │ │ ├── empty-nowdoc.php │ │ │ ├── heredoc-interpolation.php │ │ │ ├── simple-heredoc.php │ │ │ └── simple-nowdoc.php │ │ ├── echo │ │ │ ├── echo-missing-semicolon.php │ │ │ ├── echo-no-value.php │ │ │ ├── echo-single-value-trailing-comma.php │ │ │ ├── multi-echo.php │ │ │ └── simple-echo.php │ │ ├── enums │ │ │ ├── backed-enum-int.php │ │ │ ├── backed-enum-string.php │ │ │ ├── enum-with-attributes.php │ │ │ ├── enum-with-constants.php │ │ │ ├── enum-with-implements.php │ │ │ ├── enum-with-methods.php │ │ │ └── simple-enum.php │ │ ├── functions │ │ │ ├── function-with-parameter-types.php │ │ │ ├── function-with-return-type.php │ │ │ ├── simple-function-call-args-trailing-comma.php │ │ │ ├── simple-function-call-args.php │ │ │ ├── simple-function-call.php │ │ │ └── simple-function.php │ │ ├── html │ │ │ └── inline-html-with-php.php │ │ ├── identifiers │ │ │ ├── fully-qualified-identifier.php │ │ │ ├── qualified-identifier.php │ │ │ └── unqualified-identifier.php │ │ ├── interfaces │ │ │ ├── interface-with-constants.php │ │ │ ├── interface-with-extends.php │ │ │ ├── interface-with-methods.php │ │ │ └── simple-interface.php │ │ ├── literals │ │ │ ├── array-trailing-comma.php │ │ │ ├── bool.php │ │ │ ├── empty-array.php │ │ │ ├── float.php │ │ │ ├── int.php │ │ │ ├── legacy-array-single-item.php │ │ │ ├── legacy-array.php │ │ │ ├── more-nested-array.php │ │ │ ├── multi-item-array.php │ │ │ ├── nested-array.php │ │ │ ├── null.php │ │ │ ├── single-item-array.php │ │ │ └── string.php │ │ ├── logical │ │ │ ├── and.php │ │ │ ├── logical-and.php │ │ │ ├── logical-or.php │ │ │ ├── logical-xor.php │ │ │ ├── not.php │ │ │ ├── or.php │ │ │ └── xor.php │ │ ├── methods │ │ │ ├── method-with-abstract.php │ │ │ ├── method-with-attributes.php │ │ │ ├── method-with-final.php │ │ │ ├── method-with-parameters-with-default.php │ │ │ ├── method-with-parameters-with-type-and-default.php │ │ │ ├── method-with-parameters-with-type.php │ │ │ ├── method-with-parameters.php │ │ │ ├── method-with-return-type.php │ │ │ ├── method-with-static.php │ │ │ ├── method-with-visibility.php │ │ │ └── method.php │ │ ├── multi-typed-class-const.php │ │ ├── name-resolving │ │ │ ├── attribute-use-aliased.php │ │ │ ├── attribute-use.php │ │ │ ├── attribute.php │ │ │ ├── class-extends-aliased.php │ │ │ ├── class-extends-qualified-aliased.php │ │ │ ├── class-extends.php │ │ │ ├── class-implements-aliased.php │ │ │ ├── class-implements-qualified-aliased.php │ │ │ ├── class-implements.php │ │ │ ├── class-in-namespace.php │ │ │ ├── constant-namespaced.php │ │ │ ├── constant-unnamespaced.php │ │ │ ├── constant-use.php │ │ │ ├── enum-in-namespace.php │ │ │ ├── enum-static-use-alias.php │ │ │ ├── enum-static-use.php │ │ │ ├── enum-static.php │ │ │ ├── from-static-use-alias.php │ │ │ ├── from-static-use.php │ │ │ ├── from-static.php │ │ │ ├── function-argument-unqualified-use.php │ │ │ ├── function-argument-unqualified.php │ │ │ ├── function-call-qualified.php │ │ │ ├── function-call-unqualified.php │ │ │ ├── function-call-use.php │ │ │ ├── function-in-namespace.php │ │ │ ├── interface-extends.php │ │ │ ├── interface-in-namespace.php │ │ │ ├── method-argument-unqualified-use.php │ │ │ ├── method-argument-unqualified.php │ │ │ ├── new-class-aliased.php │ │ │ ├── new-class-fqn.php │ │ │ ├── new-class-qualified-aliased.php │ │ │ ├── new-class-qualified.php │ │ │ ├── new-class.php │ │ │ ├── new-parent.php │ │ │ ├── new-self.php │ │ │ ├── new-static.php │ │ │ └── trait-in-namespace.php │ │ ├── namespaces │ │ │ ├── braced-namespace.php │ │ │ ├── global-namespace.php │ │ │ └── unbraced-namespace.php │ │ ├── precedence │ │ │ ├── binary-op-multi-right-hand-assignment.php │ │ │ ├── binary-op-right-hand-assignment.php │ │ │ └── multi-op-arithmetic.php │ │ ├── property-hooks │ │ │ ├── abstract-get.php │ │ │ ├── abstract-set.php │ │ │ ├── block-body.php │ │ │ ├── empty.php │ │ │ ├── expression-body.php │ │ │ └── parameter-list.php │ │ ├── tags │ │ │ ├── echo-tag.php │ │ │ ├── empty-file.php │ │ │ ├── html.php │ │ │ ├── short-tag.php │ │ │ └── tag.php │ │ ├── traits │ │ │ ├── simple-trait.php │ │ │ ├── trait-with-methods.php │ │ │ ├── trait-with-properties.php │ │ │ └── trait-with-trait-use.php │ │ ├── uses │ │ │ ├── group-use-multiple-types.php │ │ │ ├── group-use.php │ │ │ ├── qualified-use.php │ │ │ ├── simple-use.php │ │ │ └── use-with-alias.php │ │ ├── variables │ │ │ ├── variable-variable-complex.php │ │ │ ├── variable-variable.php │ │ │ └── variable.php │ │ └── yield │ │ │ ├── empty.php │ │ │ ├── from.php │ │ │ ├── key-value.php │ │ │ └── value.php │ │ └── parser.rs ├── snappers │ ├── Cargo.toml │ ├── README.md │ ├── __snapshots__ │ │ └── it_can_say_hello_world.snap │ └── src │ │ └── lib.rs ├── span │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── token │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── type │ ├── Cargo.toml │ └── src │ └── lib.rs ├── meta ├── generate-ast.php └── generate-visitor.php └── src ├── cmd ├── index.rs ├── init.rs ├── mod.rs ├── parse.rs └── tokenise.rs ├── main.rs ├── stubs ├── .gitignore └── pxp.config.toml └── utils.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ryangjchandler] 2 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --workspace --lib --bins --tests --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | pxp.config.toml 4 | /vendor 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This document contains information related to contributing to the project and the guidelines you must follow for your contribution to be reviewed and considered. 4 | 5 | ## Picking a change 6 | 7 | It's recommended that you consult the [Issues](https://github.com/pxp-lang/pxp/issues) page before starting work. 8 | 9 | Issues are labelled based on the project that they target and the priority of the change. If you fancy a challenge, pick a "high" priority issue. Want something more casual to learn the ropes and discover the project, go for a "low" priority change. 10 | 11 | In some cases issues will be dependent on other issues. This will normally be noted on the issue itself with some text similar to "Related to X" or "Depending on X". If this _is_ the case, we recommend leaving the issue alone until the dependency has been resolved. 12 | 13 | ## Commit messages 14 | 15 | * All commit messages must be written in English. 16 | * Commit messages must be prefixed with the name of the crate(s) that is being worked on, i.e. `AST: ...`. When modifying multiple crates in a single commit message, concatenate the names with a `+` character, e.g. `AST+Parser+Type: ...`, ensuring the crate names are ordered alphabetically. 17 | * Keep commit messages as short as possible while still accurately describing the changes made. 18 | * Write commits messages in the imperative mood, e.g. `Foo: Change the way X is parsed`, not `Foo: Changed the way X is parsed`. 19 | 20 | ## Pull requests 21 | 22 | * Keep pull requests scoped to a single change. 23 | * Include a brief, but clear, description of the changes. 24 | * If the pull request closes a particular issue, add `Closes #X` to the very top of the pull request description. 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp" 3 | description = "An early-stage project to develop high-performance tools for PHP developers." 4 | version.workspace = true 5 | authors.workspace = true 6 | license-file.workspace = true 7 | rust-version.workspace = true 8 | edition.workspace = true 9 | 10 | [workspace] 11 | members = ["crates/*"] 12 | resolver = "2" 13 | 14 | [workspace.package] 15 | version = "0.1.0" 16 | authors = ["Ryan Chandler "] 17 | license-file = "LICENSE" 18 | rust-version = "1.76" 19 | edition = "2021" 20 | 21 | [profile.release] 22 | debug = true 23 | 24 | [dependencies] 25 | anyhow = "1.0.95" 26 | ariadne = { version = "0.5.0", features = ["auto-color"] } 27 | clap = { version = "4.5.23", features = ["derive", "wrap_help"] } 28 | codespan-reporting = "0.11.1" 29 | colored = "2.2.0" 30 | homedir = "0.3.4" 31 | indicatif = "0.17.9" 32 | pxp-bytestring = { version = "0.1.0", path = "crates/bytestring" } 33 | pxp-diagnostics = { version = "0.1.0", path = "crates/diagnostics" } 34 | pxp-index = { version = "0.1.0", path = "crates/index" } 35 | pxp-inference = { version = "0.1.0", path = "crates/inference" } 36 | pxp-lexer = { version = "0.1.0", path = "crates/lexer" } 37 | pxp-parser = { version = "0.1.0", path = "crates/parser" } 38 | pxp-span = { version = "0.1.0", path = "crates/span" } 39 | pxp-token = { version = "0.1.0", path = "crates/token" } 40 | pxp-type = { version = "0.1.0", path = "crates/type" } 41 | rustyline = "15.0.0" 42 | serde = { version = "1.0.216", features = ["derive"] } 43 | serde_derive = "1.0.216" 44 | toml = "0.8.19" 45 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test --workspace --lib --bins --tests 3 | 4 | ast: 5 | php ./meta/generate-ast.php 6 | php ./meta/generate-visitor.php 7 | cargo fmt --package pxp-ast 8 | 9 | meta: ast 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The PXP License, version 1.0 2 | 3 | Copyright (c) 2023 Ryan Chandler. All rights reserved. 4 | 5 | 1. You are free to use, modify and distribute this project for any purpose, whether commercial or non-commercial. 6 | 7 | 2. Modified redistributions must be made publicly available. When modifying and redistributing this project, you must: 8 | a) Clearly mention that your version of the project is modified from the original source. 9 | b) Provide a link to the original project. 10 | c) Reproduce the above copyright notice, the list of conditions and the following disclaimer at some stage. 11 | 12 | 3. Redistributed versions of this project must not use the name "PXP". You may indicate that your redistribution 13 | works in conjuction or alongside PXP by saying "Foo for PXP". 14 | 15 | 4. If your company or organisation benefits from this project, please consider supporting its ongoing development and support by sponsoring the project on GitHub. Your contributions help ensure the project's continued growth and maintenance, benefitting the entire community. 16 | 17 | 5. This project is provided "as is", with no warranties or guarantees of any kind. 18 | 19 | By using the project, you agree to adhere to the terms of this license. 20 | 21 | For more details and to access to original project, visit [pxplang.org](https://pxplang.org). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](/art/hero.png) 2 | 3 | > [!NOTE] 4 | > No longer under active development. [Read more here](https://ryangjchandler.co.uk/posts/saying-goodbye-to-pxp). 5 | 6 | A suite of high-performance tools for PHP developers. Written in Rust, designed for performance and reliability. 7 | 8 | ## Credits 9 | 10 | This project is maintained by [Ryan Chandler](https://twitter.com/ryangjchandler) with the help of [contributors](https://github.com/pxp-lang/pxp/graphs/contributors). 11 | -------------------------------------------------------------------------------- /art/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryangjchandler/pxp/d63ec46c22c8d00e746de650a31288c0853e9ca4/art/hero.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "nikic/php-parser": "^5.4", 4 | "illuminate/support": "^11.38", 5 | "symfony/yaml": "^7.2", 6 | "symfony/var-dumper": "^7.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crates/ast/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-ast" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-token = { path = "../token" } 11 | pxp-span = { path = "../span" } 12 | pxp-type = { path = "../type" } 13 | pxp-bytestring = { path = "../bytestring" } 14 | -------------------------------------------------------------------------------- /crates/ast/README.md: -------------------------------------------------------------------------------- 1 | # AST 2 | 3 | This crate contains all of the code related to representing PHP and PXP code. 4 | 5 | ## Overview 6 | 7 | There are 2 main structures for representing code: 8 | * `Statement` 9 | * `Expression` 10 | 11 | Both of these are represented with 3 fields: 12 | * `kind` - represents the type of node. 13 | * `span` - the location of the node in the source text. 14 | * `comments` - any comments attached to the node. 15 | 16 | Despite the name of the crate, the nodes actually form a _"concrete syntax tree"_ and not an abstract one. This means that every single piece of information is stored, including the positions of punctuation, keywords, etc. -------------------------------------------------------------------------------- /crates/ast/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | println!("cargo:rerun-if-changed=meta/ast.yaml"); 5 | 6 | if file_is_newer_than("meta/ast.yaml".into(), "src/generated.rs".into()) { 7 | panic!("AST definition is newer than generated code. Please run `just generate-ast` to update the generated code."); 8 | } 9 | } 10 | 11 | fn file_is_newer_than(file: PathBuf, other: PathBuf) -> bool { 12 | let file_metadata = std::fs::metadata(file).unwrap(); 13 | let other_metadata = std::fs::metadata(other).unwrap(); 14 | 15 | file_metadata.modified().unwrap() > other_metadata.modified().unwrap() 16 | } 17 | -------------------------------------------------------------------------------- /crates/ast/src/array.rs: -------------------------------------------------------------------------------- 1 | use crate::{ArrayExpression, ArrayItem, Expression, HasId}; 2 | 3 | impl ArrayExpression { 4 | pub fn is_list(&self) -> bool { 5 | self.items 6 | .iter() 7 | .all(|item| matches!(item, ArrayItem::Value(_))) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/ast/src/backed_enum_type.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::BackedEnumType; 4 | 5 | impl BackedEnumType { 6 | pub fn is_valid(&self) -> bool { 7 | match self { 8 | Self::String(..) | Self::Int(..) => true, 9 | Self::Invalid => false, 10 | } 11 | } 12 | } 13 | 14 | impl Display for BackedEnumType { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | match self { 17 | BackedEnumType::String(..) => write!(f, "string"), 18 | BackedEnumType::Int(..) => write!(f, "int"), 19 | BackedEnumType::Invalid => write!(f, "invalid"), 20 | } 21 | } 22 | } 23 | 24 | impl Default for BackedEnumType { 25 | fn default() -> Self { 26 | Self::Invalid 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/ast/src/comments.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | 3 | use pxp_span::{IsSpanned, Span}; 4 | 5 | use crate::{Comment, CommentGroup}; 6 | 7 | impl IsSpanned for CommentGroup { 8 | fn span(&self) -> Span { 9 | self.comments.span() 10 | } 11 | } 12 | 13 | impl CommentGroup { 14 | pub fn iter(&self) -> Iter<'_, Comment> { 15 | self.comments.iter() 16 | } 17 | } 18 | 19 | impl IntoIterator for CommentGroup { 20 | type Item = Comment; 21 | type IntoIter = std::vec::IntoIter; 22 | 23 | fn into_iter(self) -> Self::IntoIter { 24 | self.comments.into_iter() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/ast/src/data_type.rs: -------------------------------------------------------------------------------- 1 | use pxp_span::Span; 2 | use pxp_type::Type; 3 | 4 | use crate::{DataType, NodeId, ResolvedName}; 5 | 6 | impl DataType { 7 | pub fn new(id: NodeId, kind: Type, span: Span) -> Self { 8 | Self { id, kind, span } 9 | } 10 | 11 | pub fn get_type(&self) -> &Type { 12 | &self.kind 13 | } 14 | 15 | pub fn get_span(&self) -> Span { 16 | self.span 17 | } 18 | 19 | pub fn standalone(&self) -> bool { 20 | self.kind.standalone() 21 | } 22 | 23 | pub fn nullable(&self) -> bool { 24 | self.kind.nullable() 25 | } 26 | 27 | pub fn includes_callable(&self) -> bool { 28 | self.kind.includes_callable() 29 | } 30 | 31 | pub fn is_bottom(&self) -> bool { 32 | self.kind.is_bottom() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/ast/src/id.rs: -------------------------------------------------------------------------------- 1 | use crate::NodeId; 2 | 3 | pub trait HasId { 4 | fn id(&self) -> NodeId; 5 | } 6 | 7 | impl HasId for Box { 8 | fn id(&self) -> NodeId { 9 | self.as_ref().id() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/ast/src/identifiers.rs: -------------------------------------------------------------------------------- 1 | use pxp_bytestring::ByteString; 2 | use pxp_span::{IsSpanned, Span}; 3 | 4 | use crate::{Identifier, NodeId, SimpleIdentifier}; 5 | 6 | impl Identifier { 7 | pub fn missing(id: NodeId, span: Span) -> Self { 8 | Self::SimpleIdentifier(SimpleIdentifier::new(id, ByteString::empty(), span)) 9 | } 10 | 11 | pub fn is_missing(&self) -> bool { 12 | self.is_simple() && self.to_simple().is_missing() 13 | } 14 | 15 | pub fn to_simple(&self) -> &SimpleIdentifier { 16 | match self { 17 | Self::SimpleIdentifier(simple) => simple, 18 | Self::DynamicIdentifier(..) => unreachable!(), 19 | } 20 | } 21 | 22 | pub fn is_simple(&self) -> bool { 23 | match self { 24 | Self::SimpleIdentifier(..) => true, 25 | Self::DynamicIdentifier(..) => false, 26 | } 27 | } 28 | 29 | pub fn is_dynamic(&self) -> bool { 30 | match self { 31 | Self::SimpleIdentifier(..) => false, 32 | Self::DynamicIdentifier(..) => true, 33 | } 34 | } 35 | } 36 | 37 | impl IsSpanned for Identifier { 38 | fn span(&self) -> Span { 39 | match self { 40 | Self::SimpleIdentifier(simple) => simple.span, 41 | Self::DynamicIdentifier(dynamic) => dynamic.span, 42 | } 43 | } 44 | } 45 | 46 | impl SimpleIdentifier { 47 | pub fn new(id: NodeId, symbol: ByteString, span: Span) -> Self { 48 | Self { id, symbol, span } 49 | } 50 | 51 | pub fn missing(id: NodeId, span: Span) -> Self { 52 | Self::new(id, ByteString::empty(), span) 53 | } 54 | 55 | pub fn is_missing(&self) -> bool { 56 | self.symbol.is_empty() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/ast/src/literals.rs: -------------------------------------------------------------------------------- 1 | use pxp_span::Span; 2 | use pxp_token::{OwnedToken, Token}; 3 | 4 | use crate::{Literal, LiteralKind, NodeId}; 5 | 6 | impl Literal { 7 | pub fn new(id: NodeId, kind: LiteralKind, token: OwnedToken, span: Span) -> Literal { 8 | Literal { 9 | id, 10 | kind, 11 | token, 12 | span, 13 | } 14 | } 15 | 16 | pub fn missing(id: NodeId, span: Span) -> Literal { 17 | Literal { 18 | id, 19 | kind: LiteralKind::Missing, 20 | token: Token::missing(span).to_owned(), 21 | span, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/ast/src/node.rs: -------------------------------------------------------------------------------- 1 | use pxp_span::Span; 2 | 3 | use crate::{NodeId, NodeKind}; 4 | 5 | #[derive(Debug, PartialEq, Clone, Copy)] 6 | pub struct Node<'a> { 7 | pub id: NodeId, 8 | pub kind: NodeKind<'a>, 9 | pub span: Span, 10 | } 11 | -------------------------------------------------------------------------------- /crates/ast/src/properties.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | HookedProperty, InitializedPropertyEntry, Property, PropertyEntryKind, PropertyModifierGroup, 3 | SimpleProperty, SimpleVariable, UninitializedPropertyEntry, 4 | }; 5 | 6 | impl Property { 7 | pub fn modifiers(&self) -> &PropertyModifierGroup { 8 | match self { 9 | Property::Simple(SimpleProperty { modifiers, .. }) => modifiers, 10 | Property::Hooked(HookedProperty { modifiers, .. }) => modifiers, 11 | } 12 | } 13 | 14 | pub fn is_public(&self) -> bool { 15 | self.modifiers().is_public() 16 | } 17 | 18 | pub fn is_protected(&self) -> bool { 19 | self.modifiers().is_protected() 20 | } 21 | 22 | pub fn is_private(&self) -> bool { 23 | self.modifiers().is_private() 24 | } 25 | 26 | pub fn is_static(&self) -> bool { 27 | self.modifiers().has_static() 28 | } 29 | 30 | pub fn is_readonly(&self) -> bool { 31 | self.modifiers().has_readonly() 32 | } 33 | } 34 | 35 | impl PropertyEntryKind { 36 | pub fn variable(&self) -> &SimpleVariable { 37 | match self { 38 | PropertyEntryKind::Uninitialized(UninitializedPropertyEntry { variable, .. }) => { 39 | variable 40 | } 41 | PropertyEntryKind::Initialized(InitializedPropertyEntry { variable, .. }) => variable, 42 | } 43 | } 44 | 45 | pub fn is_initialized(&self) -> bool { 46 | match self { 47 | PropertyEntryKind::Uninitialized { .. } => false, 48 | PropertyEntryKind::Initialized { .. } => true, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/ast/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | use std::slice::IterMut; 3 | 4 | use pxp_span::IsSpanned; 5 | use pxp_span::Span; 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone)] 8 | 9 | pub struct CommaSeparated { 10 | pub inner: Vec, 11 | pub commas: Vec, // `,` 12 | } 13 | 14 | impl CommaSeparated { 15 | pub fn iter(&self) -> Iter<'_, T> { 16 | self.inner.iter() 17 | } 18 | 19 | pub fn iter_mut(&mut self) -> IterMut<'_, T> { 20 | self.inner.iter_mut() 21 | } 22 | 23 | pub fn len(&self) -> usize { 24 | self.inner.len() 25 | } 26 | 27 | pub fn is_empty(&self) -> bool { 28 | self.inner.is_empty() 29 | } 30 | } 31 | 32 | impl IsSpanned for CommaSeparated { 33 | fn span(&self) -> Span { 34 | self.inner.span() 35 | } 36 | } 37 | 38 | impl IntoIterator for CommaSeparated { 39 | type Item = T; 40 | type IntoIter = std::vec::IntoIter; 41 | 42 | fn into_iter(self) -> Self::IntoIter { 43 | self.inner.into_iter() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/ast/src/variables.rs: -------------------------------------------------------------------------------- 1 | use crate::{NodeId, SimpleVariable, Variable}; 2 | use pxp_bytestring::ByteString; 3 | use pxp_span::{IsSpanned, Span}; 4 | 5 | impl SimpleVariable { 6 | pub fn missing(id: NodeId, span: Span) -> Self { 7 | Self { 8 | id, 9 | symbol: ByteString::empty(), 10 | stripped: ByteString::empty(), 11 | span, 12 | } 13 | } 14 | 15 | pub fn is_missing(&self) -> bool { 16 | self.symbol.is_empty() 17 | } 18 | } 19 | 20 | impl IsSpanned for Variable { 21 | fn span(&self) -> Span { 22 | match self { 23 | Self::SimpleVariable(simple) => simple.span, 24 | Self::VariableVariable(dynamic) => dynamic.span, 25 | Self::BracedVariableVariable(dynamic) => dynamic.span, 26 | } 27 | } 28 | } 29 | 30 | impl Variable { 31 | pub fn to_simple(&self) -> &SimpleVariable { 32 | match self { 33 | Self::SimpleVariable(simple) => simple, 34 | _ => unreachable!(), 35 | } 36 | } 37 | 38 | pub fn is_simple(&self) -> bool { 39 | matches!(self, Self::SimpleVariable(_)) 40 | } 41 | 42 | pub fn is_variable(&self) -> bool { 43 | matches!( 44 | self, 45 | Self::VariableVariable(_) | Self::BracedVariableVariable(_) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/ast/src/visibility.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] 4 | pub enum Visibility { 5 | #[default] 6 | Public, 7 | Protected, 8 | Private, 9 | } 10 | 11 | impl Display for Visibility { 12 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Visibility::Public => write!(f, "public"), 15 | Visibility::Protected => write!(f, "protected"), 16 | Visibility::Private => write!(f, "private"), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/ast/src/visitor/mod.rs: -------------------------------------------------------------------------------- 1 | mod immutable; 2 | mod mutable; 3 | mod node; 4 | mod walk; 5 | mod walk_mut; 6 | 7 | pub use immutable::Visitor; 8 | pub use mutable::VisitorMut; 9 | pub use node::{Ancestors, NodeVisitor, NodeVisitorEscapeHatch}; 10 | pub use walk::*; 11 | pub use walk_mut::*; 12 | -------------------------------------------------------------------------------- /crates/ast/src/visitor/node.rs: -------------------------------------------------------------------------------- 1 | use crate::{Node, NodeKind, Statement}; 2 | 3 | #[derive(PartialEq, Eq)] 4 | pub enum NodeVisitorEscapeHatch { 5 | SkipChildren, 6 | Stop, 7 | Continue, 8 | } 9 | 10 | pub trait NodeVisitor<'a> { 11 | fn enter(&mut self, _: Node<'a>, _: &mut Ancestors<'a>) -> NodeVisitorEscapeHatch { 12 | NodeVisitorEscapeHatch::Continue 13 | } 14 | 15 | fn leave(&mut self, _: Node<'a>, _: &mut Ancestors<'a>) -> NodeVisitorEscapeHatch { 16 | NodeVisitorEscapeHatch::Continue 17 | } 18 | 19 | fn visit(&mut self, node: Node<'a>, ancestors: &mut Ancestors<'a>) -> NodeVisitorEscapeHatch { 20 | let mut escape = self.enter(node, ancestors); 21 | 22 | ancestors.push(node); 23 | 24 | if escape != NodeVisitorEscapeHatch::SkipChildren { 25 | for child in node.children() { 26 | escape = self.visit(child, ancestors); 27 | 28 | if escape == NodeVisitorEscapeHatch::Stop { 29 | return NodeVisitorEscapeHatch::Stop; 30 | } 31 | } 32 | } else if escape == NodeVisitorEscapeHatch::Stop { 33 | return NodeVisitorEscapeHatch::Stop; 34 | } 35 | 36 | ancestors.pop(); 37 | 38 | self.leave(node, ancestors) 39 | } 40 | 41 | fn traverse(&mut self, ast: &'a [Statement]) { 42 | let mut ancestors = Ancestors::new(); 43 | 44 | for statement in ast { 45 | let escape = self.visit( 46 | Node::new(statement.id, NodeKind::Statement(statement), statement.span), 47 | &mut ancestors, 48 | ); 49 | 50 | if escape == NodeVisitorEscapeHatch::Stop { 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct Ancestors<'a> { 59 | ancestors: Vec>, 60 | } 61 | 62 | impl<'a> Ancestors<'a> { 63 | fn new() -> Self { 64 | Self { 65 | ancestors: Vec::new(), 66 | } 67 | } 68 | 69 | fn push(&mut self, node: Node<'a>) { 70 | self.ancestors.push(node); 71 | } 72 | 73 | fn pop(&mut self) { 74 | self.ancestors.pop(); 75 | } 76 | 77 | pub fn last(&self) -> Option> { 78 | self.ancestors.last().cloned() 79 | } 80 | 81 | pub fn find(&self, cb: impl Fn(&Node<'a>) -> bool) -> Option> { 82 | self.ancestors.iter().rev().find(|node| cb(node)).cloned() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/bytestring/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-bytestring" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /crates/bytestring/README.md: -------------------------------------------------------------------------------- 1 | # ByteString 2 | 3 | This crate contains a set of helper structures for representing and accurately printing byte-sequences. 4 | 5 | ## Overview 6 | 7 | Since PHP code is not required to be valid UTF-8, we can't use Rust's regular `String` and `&str` values to represent identifiers, variables, etc. Instead, we have to operate on sequences of raw bytes (`u8` values). 8 | 9 | The painful part of this is that Rust doesn't have any "clean" ways to print or debug these values, so we instead have two helper structures for owned `Vec` sequences and borrowed `&[u8]` sequences: 10 | * `ByteString` 11 | * `ByteStr` 12 | 13 | Both of these structures can be created from a sequence of bytes and in nearly all cases treated as their underlying data types. -------------------------------------------------------------------------------- /crates/bytestring/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod bytestr; 2 | mod bytestring; 3 | 4 | pub use bytestr::*; 5 | pub use bytestring::*; 6 | -------------------------------------------------------------------------------- /crates/diagnostics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-diagnostics" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-span = { path = "../span" } 11 | pxp-token = { path = "../token" } 12 | -------------------------------------------------------------------------------- /crates/diagnostics/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod severity; 2 | 3 | use pxp_span::Span; 4 | pub use severity::*; 5 | 6 | pub trait DiagnosticKind { 7 | fn get_code(&self) -> String; 8 | fn get_identifier(&self) -> String; 9 | fn get_message(&self) -> String; 10 | fn get_help(&self) -> Option { 11 | None 12 | } 13 | fn get_labels(&self) -> Vec { 14 | Vec::new() 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct DiagnosticLabel { 20 | pub style: DiagnosticLabelStyle, 21 | pub span: Span, 22 | pub message: String, 23 | } 24 | 25 | impl DiagnosticLabel { 26 | pub fn new(style: DiagnosticLabelStyle, span: Span, message: impl Into) -> Self { 27 | Self { 28 | style, 29 | span, 30 | message: message.into(), 31 | } 32 | } 33 | 34 | pub fn primary(span: Span, message: impl Into) -> Self { 35 | Self::new(DiagnosticLabelStyle::Primary, span, message) 36 | } 37 | 38 | pub fn secondary(span: Span, message: impl Into) -> Self { 39 | Self::new(DiagnosticLabelStyle::Secondary, span, message) 40 | } 41 | } 42 | 43 | #[derive(Debug, Clone, Copy)] 44 | pub enum DiagnosticLabelStyle { 45 | Primary, 46 | Secondary, 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct Diagnostic { 51 | pub kind: K, 52 | pub severity: Severity, 53 | pub span: Span, 54 | } 55 | 56 | impl Diagnostic { 57 | pub fn new(kind: K, severity: Severity, span: Span) -> Self { 58 | Self { 59 | kind, 60 | severity, 61 | span, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/diagnostics/src/severity.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum Severity { 5 | Hint, 6 | Information, 7 | Warning, 8 | Error, 9 | } 10 | 11 | impl Severity { 12 | pub fn is_error(&self) -> bool { 13 | self == &Severity::Error 14 | } 15 | 16 | pub fn is_warning(&self) -> bool { 17 | self == &Severity::Warning 18 | } 19 | 20 | pub fn is_information(&self) -> bool { 21 | self == &Severity::Information 22 | } 23 | 24 | pub fn is_hint(&self) -> bool { 25 | self == &Severity::Hint 26 | } 27 | } 28 | 29 | impl Display for Severity { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | match self { 32 | Severity::Hint => write!(f, "[hint]",), 33 | Severity::Information => write!(f, "[info]",), 34 | Severity::Warning => write!(f, "[warning]",), 35 | Severity::Error => write!(f, "[error]",), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crates/index/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-index" 3 | description = "Index information about a PHP project." 4 | version.workspace = true 5 | authors.workspace = true 6 | license-file.workspace = true 7 | rust-version.workspace = true 8 | edition.workspace = true 9 | 10 | [dependencies] 11 | pxp-ast = { version = "0.1.0", path = "../ast" } 12 | pxp-bytestring = { version = "0.1.0", path = "../bytestring" } 13 | pxp-lexer = { version = "0.1.0", path = "../lexer" } 14 | pxp-parser = { version = "0.1.0", path = "../parser" } 15 | pxp-span = { version = "0.1.0", path = "../span" } 16 | pxp-type = { version = "0.1.0", path = "../type" } 17 | 18 | [dev-dependencies] 19 | discoverer = "0.2.0" 20 | -------------------------------------------------------------------------------- /crates/index/src/entities/class.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::ResolvedName; 2 | 3 | use crate::{location::Location, HasFileId}; 4 | 5 | use super::MethodEntity; 6 | 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct ClassEntity { 9 | pub(crate) name: ResolvedName, 10 | pub(crate) kind: ClassEntityKind, 11 | pub(crate) methods: Vec, 12 | pub(crate) location: Location, 13 | } 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq)] 16 | pub enum ClassEntityKind { 17 | Class, 18 | Interface, 19 | Enum, 20 | Trait, 21 | } 22 | 23 | impl HasFileId for ClassEntity { 24 | fn file_id(&self) -> crate::FileId { 25 | self.location.file_id() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/index/src/entities/function.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{Name, ResolvedName}; 2 | use pxp_type::Type; 3 | 4 | use crate::{location::Location, FileId, HasFileId}; 5 | 6 | use super::parameters::Parameters; 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct FunctionEntity { 10 | pub(crate) name: ResolvedName, 11 | pub(crate) parameters: Parameters, 12 | pub(crate) return_type: Option>, 13 | pub(crate) returns_reference: bool, 14 | pub(crate) location: Location, 15 | } 16 | 17 | impl HasFileId for FunctionEntity { 18 | fn file_id(&self) -> FileId { 19 | self.location.file_id() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/index/src/entities/method.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{MethodModifierGroup, Name, ResolvedName, SimpleIdentifier}; 2 | use pxp_type::Type; 3 | 4 | use crate::{location::Location, HasFileId}; 5 | 6 | use super::Parameters; 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct MethodEntity { 10 | pub(crate) name: SimpleIdentifier, 11 | pub(crate) parameters: Parameters, 12 | pub(crate) return_type: Option>, 13 | pub(crate) returns_reference: bool, 14 | pub(crate) modifiers: MethodModifierGroup, 15 | pub(crate) location: Location, 16 | } 17 | 18 | impl HasFileId for MethodEntity { 19 | fn file_id(&self) -> crate::FileId { 20 | self.location.file_id() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/index/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | mod class; 2 | mod function; 3 | mod method; 4 | mod parameters; 5 | 6 | pub use class::{ClassEntity, ClassEntityKind}; 7 | pub use function::FunctionEntity; 8 | pub use method::MethodEntity; 9 | pub use parameters::{Parameter, Parameters}; 10 | use pxp_bytestring::ByteString; 11 | 12 | #[derive(Debug, Clone, Default)] 13 | pub(crate) struct EntityRegistry { 14 | functions: Vec, 15 | classes: Vec, 16 | } 17 | 18 | impl EntityRegistry { 19 | pub fn add_function(&mut self, function: FunctionEntity) { 20 | self.functions.push(function); 21 | } 22 | 23 | pub fn functions(&self) -> &[FunctionEntity] { 24 | &self.functions 25 | } 26 | 27 | pub fn get_function(&self, name: impl Into) -> Option<&FunctionEntity> { 28 | let name = name.into(); 29 | 30 | self.functions.iter().find(|f| f.name.resolved == name) 31 | } 32 | 33 | pub fn add_class(&mut self, class: ClassEntity) { 34 | self.classes.push(class); 35 | } 36 | 37 | pub fn classes(&self) -> &[ClassEntity] { 38 | &self.classes 39 | } 40 | 41 | pub fn get_class(&self, name: impl Into) -> Option<&ClassEntity> { 42 | let name = name.into(); 43 | 44 | self.classes.iter().find(|c| c.name.resolved == name) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/index/src/entities/parameters.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{Name, ResolvedName, SimpleVariable}; 2 | use pxp_type::Type; 3 | 4 | use crate::location::Location; 5 | 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub struct Parameters { 8 | parameters: Vec, 9 | } 10 | 11 | impl Parameters { 12 | pub fn new(parameters: Vec) -> Self { 13 | Self { parameters } 14 | } 15 | 16 | pub fn len(&self) -> usize { 17 | self.parameters.len() 18 | } 19 | 20 | pub fn is_empty(&self) -> bool { 21 | self.parameters.is_empty() 22 | } 23 | 24 | pub fn iter(&self) -> impl Iterator { 25 | self.parameters.iter() 26 | } 27 | } 28 | 29 | #[derive(Debug, Clone, PartialEq)] 30 | pub struct Parameter { 31 | pub(crate) name: SimpleVariable, 32 | pub(crate) r#type: Option>, 33 | pub(crate) optional: bool, 34 | pub(crate) variadic: bool, 35 | pub(crate) location: Location, 36 | } 37 | -------------------------------------------------------------------------------- /crates/index/src/file.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 7 | pub struct FileId(usize); 8 | 9 | impl FileId { 10 | pub fn new(id: usize) -> Self { 11 | Self(id) 12 | } 13 | } 14 | 15 | pub trait HasFileId { 16 | fn file_id(&self) -> FileId; 17 | } 18 | 19 | impl HasFileId for FileId { 20 | fn file_id(&self) -> FileId { 21 | *self 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone, Default)] 26 | pub(crate) struct FileRegistry { 27 | files: HashMap, 28 | } 29 | 30 | impl FileRegistry { 31 | pub fn get_file_path(&self, id: FileId) -> Option<&Path> { 32 | self.files 33 | .iter() 34 | .find_map(|(path, &file_id)| -> Option<&Path> { 35 | if file_id == id { 36 | Some(path) 37 | } else { 38 | None 39 | } 40 | }) 41 | } 42 | 43 | pub fn get_file_path_unchecked(&self, id: FileId) -> &Path { 44 | self.get_file_path(id).unwrap() 45 | } 46 | 47 | pub fn get_or_insert(&mut self, path: &Path) -> FileId { 48 | if let Some(&id) = self.files.get(path) { 49 | id 50 | } else { 51 | let id = FileId(self.files.len()); 52 | self.files.insert(path.to_path_buf(), id); 53 | id 54 | } 55 | } 56 | 57 | pub fn len(&self) -> usize { 58 | self.files.len() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/index/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use entities::EntityRegistry; 4 | use file::FileRegistry; 5 | 6 | mod entities; 7 | mod file; 8 | mod indexer; 9 | mod location; 10 | mod reflection; 11 | 12 | pub use file::{FileId, HasFileId}; 13 | use indexer::IndexingVisitor; 14 | use pxp_ast::{visitor::Visitor, Statement}; 15 | use pxp_bytestring::ByteString; 16 | use pxp_lexer::Lexer; 17 | use pxp_parser::Parser; 18 | 19 | pub use entities::{FunctionEntity, Parameter, Parameters}; 20 | pub use location::{HasLocation, Location}; 21 | pub use reflection::{ 22 | ReflectionClass, ReflectionFunction, ReflectionFunctionLike, ReflectionParameter, 23 | ReflectsParameters, ReflectionType, 24 | }; 25 | 26 | #[derive(Debug, Clone, Default)] 27 | pub struct Index { 28 | files: FileRegistry, 29 | pub(crate) entities: EntityRegistry, 30 | } 31 | 32 | impl Index { 33 | pub fn new() -> Self { 34 | Self::default() 35 | } 36 | 37 | pub fn index_file(&mut self, path: &Path) { 38 | let file_id = self.files.get_or_insert(path); 39 | let contents = std::fs::read(path).unwrap(); 40 | let parse_result = Parser::parse(Lexer::new(&contents)); 41 | 42 | self.index(file_id, &parse_result.ast); 43 | } 44 | 45 | pub fn index(&mut self, file_id: FileId, ast: &[Statement]) { 46 | let mut visitor = IndexingVisitor::new(file_id, self); 47 | visitor.visit(ast); 48 | } 49 | 50 | pub fn number_of_files(&self) -> usize { 51 | self.files.len() 52 | } 53 | 54 | pub fn number_of_functions(&self) -> usize { 55 | self.entities.functions().len() 56 | } 57 | 58 | pub fn get_function(&self, name: impl Into) -> Option { 59 | self.entities 60 | .get_function(name) 61 | .map(ReflectionFunction::new) 62 | } 63 | 64 | pub fn number_of_classes(&self) -> usize { 65 | self.entities.classes().len() 66 | } 67 | 68 | pub fn get_class(&self, name: impl Into) -> Option { 69 | self.entities.get_class(name).map(ReflectionClass::new) 70 | } 71 | 72 | pub fn get_file_path(&self, from: impl HasFileId) -> Option<&std::path::Path> { 73 | self.files.get_file_path(from.file_id()) 74 | } 75 | 76 | pub fn get_file_path_unchecked(&self, from: impl HasFileId) -> &std::path::Path { 77 | self.files.get_file_path_unchecked(from.file_id()) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/index/src/location.rs: -------------------------------------------------------------------------------- 1 | use pxp_span::{IsSpanned, Span}; 2 | 3 | use crate::{FileId, HasFileId}; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | pub struct Location { 7 | file: FileId, 8 | span: Span, 9 | } 10 | 11 | impl Location { 12 | pub fn new(file: FileId, span: Span) -> Self { 13 | Self { file, span } 14 | } 15 | } 16 | 17 | impl IsSpanned for Location { 18 | fn span(&self) -> Span { 19 | self.span 20 | } 21 | } 22 | 23 | impl HasFileId for Location { 24 | fn file_id(&self) -> FileId { 25 | self.file 26 | } 27 | } 28 | 29 | pub trait HasLocation { 30 | fn location(&self) -> Location; 31 | } 32 | -------------------------------------------------------------------------------- /crates/index/src/reflection/class.rs: -------------------------------------------------------------------------------- 1 | use pxp_bytestring::ByteStr; 2 | 3 | use crate::{ 4 | entities::{ClassEntity, ClassEntityKind}, 5 | location::{HasLocation, Location}, 6 | }; 7 | 8 | use super::ReflectionMethod; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | pub struct ReflectionClass<'a> { 12 | entity: &'a ClassEntity, 13 | } 14 | 15 | impl<'a> HasLocation for ReflectionClass<'a> { 16 | fn location(&self) -> Location { 17 | self.entity.location 18 | } 19 | } 20 | 21 | impl<'a> ReflectionClass<'a> { 22 | pub fn new(entity: &'a ClassEntity) -> Self { 23 | Self { entity } 24 | } 25 | 26 | pub fn name(&self) -> &ByteStr { 27 | self.entity.name.resolved.as_ref() 28 | } 29 | 30 | pub fn short_name(&self) -> &ByteStr { 31 | self.entity.name.original.as_ref() 32 | } 33 | 34 | pub fn is_class(&self) -> bool { 35 | self.entity.kind == ClassEntityKind::Class 36 | } 37 | 38 | pub fn is_interface(&self) -> bool { 39 | self.entity.kind == ClassEntityKind::Interface 40 | } 41 | 42 | pub fn is_enum(&self) -> bool { 43 | self.entity.kind == ClassEntityKind::Enum 44 | } 45 | 46 | pub fn is_trait(&self) -> bool { 47 | self.entity.kind == ClassEntityKind::Trait 48 | } 49 | 50 | pub fn get_methods(&self) -> Vec { 51 | self.entity 52 | .methods 53 | .iter() 54 | .map(|m| ReflectionMethod::new(m, self)) 55 | .collect() 56 | } 57 | 58 | pub fn get_method(&self, name: &ByteStr) -> Option { 59 | self.get_methods() 60 | .into_iter() 61 | .find(|method| method.get_name() == name) 62 | } 63 | 64 | pub fn get_static_methods(&self) -> Vec { 65 | self.get_methods() 66 | .into_iter() 67 | .filter(|method| method.is_static()) 68 | .collect() 69 | } 70 | 71 | pub fn get_static_method(&self, name: &ByteStr) -> Option { 72 | self.get_static_methods() 73 | .into_iter() 74 | .find(|method| method.get_name() == name) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/index/src/reflection/function.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{Name, ResolvedName}; 2 | use pxp_bytestring::ByteStr; 3 | use pxp_type::Type; 4 | 5 | use crate::{ 6 | location::{HasLocation, Location}, 7 | FunctionEntity, 8 | }; 9 | 10 | use super::{parameters::{CanReflectParameters, ReflectionParameter, ReflectsParameters}, ReflectionType}; 11 | 12 | #[derive(Debug, Clone, Copy, PartialEq)] 13 | pub struct ReflectionFunction<'a> { 14 | pub(crate) entity: &'a FunctionEntity, 15 | } 16 | 17 | impl<'a> ReflectionFunction<'a> { 18 | pub fn new(entity: &'a FunctionEntity) -> Self { 19 | Self { entity } 20 | } 21 | 22 | pub fn get_name(&self) -> &ByteStr { 23 | self.entity.name.resolved.as_ref() 24 | } 25 | 26 | pub fn get_short_name(&self) -> &ByteStr { 27 | self.entity.name.original.as_ref() 28 | } 29 | 30 | pub fn in_namespace(&self) -> bool { 31 | self.entity.name.resolved != self.entity.name.original 32 | } 33 | } 34 | 35 | impl<'a> HasLocation for ReflectionFunction<'a> { 36 | fn location(&self) -> Location { 37 | self.entity.location 38 | } 39 | } 40 | 41 | impl CanReflectParameters for ReflectionFunction<'_> {} 42 | 43 | impl<'a> ReflectsParameters<'a, ReflectionFunction<'a>> for ReflectionFunction<'a> { 44 | fn get_parameters(&self) -> Vec>> { 45 | self.entity 46 | .parameters 47 | .iter() 48 | .map(|p| ReflectionParameter::new(p, *self)) 49 | .collect() 50 | } 51 | } 52 | 53 | impl IsFunctionLike for ReflectionFunction<'_> {} 54 | 55 | impl<'a> ReflectionFunctionLike<'a> for ReflectionFunction<'a> { 56 | fn get_return_type(&self) -> Option> { 57 | self.entity.return_type.as_ref().map(|t| ReflectionType::new(t)) 58 | } 59 | 60 | fn returns_reference(&self) -> bool { 61 | self.entity.returns_reference 62 | } 63 | } 64 | 65 | pub trait IsFunctionLike {} 66 | 67 | pub trait ReflectionFunctionLike<'a>: IsFunctionLike { 68 | fn get_return_type(&self) -> Option>; 69 | 70 | fn has_return_type(&self) -> bool { 71 | self.get_return_type().is_some() 72 | } 73 | 74 | fn returns_reference(&self) -> bool; 75 | } 76 | -------------------------------------------------------------------------------- /crates/index/src/reflection/method.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{Name, ResolvedName}; 2 | use pxp_bytestring::ByteStr; 3 | use pxp_type::Type; 4 | 5 | use crate::{ 6 | entities::MethodEntity, 7 | location::{HasLocation, Location}, 8 | }; 9 | 10 | use super::{ 11 | function::{IsFunctionLike, ReflectionFunctionLike}, 12 | parameters::{CanReflectParameters, ReflectsParameters}, 13 | ReflectionClass, ReflectionParameter, ReflectionType, 14 | }; 15 | 16 | #[derive(Debug, Clone, Copy, PartialEq)] 17 | pub struct ReflectionMethod<'a> { 18 | pub(crate) entity: &'a MethodEntity, 19 | pub(crate) owner: &'a ReflectionClass<'a>, 20 | } 21 | 22 | impl<'a> HasLocation for ReflectionMethod<'a> { 23 | fn location(&self) -> Location { 24 | self.entity.location 25 | } 26 | } 27 | 28 | impl<'a> ReflectionMethod<'a> { 29 | pub fn new(entity: &'a MethodEntity, owner: &'a ReflectionClass<'a>) -> Self { 30 | Self { entity, owner } 31 | } 32 | 33 | pub fn get_name(&self) -> &ByteStr { 34 | self.entity.name.symbol.as_ref() 35 | } 36 | 37 | pub fn get_class(&self) -> &ReflectionClass<'a> { 38 | self.owner 39 | } 40 | 41 | pub fn is_public(&self) -> bool { 42 | self.entity.modifiers.is_public() 43 | } 44 | 45 | pub fn is_protected(&self) -> bool { 46 | self.entity.modifiers.is_protected() 47 | } 48 | 49 | pub fn is_private(&self) -> bool { 50 | self.entity.modifiers.is_private() 51 | } 52 | 53 | pub fn is_static(&self) -> bool { 54 | self.entity.modifiers.has_static() 55 | } 56 | 57 | pub fn is_final(&self) -> bool { 58 | self.entity.modifiers.has_final() 59 | } 60 | 61 | pub fn is_abstract(&self) -> bool { 62 | self.entity.modifiers.has_abstract() 63 | } 64 | } 65 | 66 | impl CanReflectParameters for ReflectionMethod<'_> {} 67 | 68 | impl<'a> ReflectsParameters<'a, ReflectionMethod<'a>> for ReflectionMethod<'a> { 69 | fn get_parameters(&self) -> Vec>> { 70 | self.entity 71 | .parameters 72 | .iter() 73 | .map(|p| ReflectionParameter::new(p, *self)) 74 | .collect() 75 | } 76 | } 77 | 78 | impl IsFunctionLike for ReflectionMethod<'_> {} 79 | 80 | impl<'a> ReflectionFunctionLike<'a> for ReflectionMethod<'a> { 81 | fn get_return_type(&self) -> Option> { 82 | self.entity.return_type.as_ref().map(|t| ReflectionType::new(t)) 83 | } 84 | 85 | fn returns_reference(&self) -> bool { 86 | self.entity.returns_reference 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/index/src/reflection/mod.rs: -------------------------------------------------------------------------------- 1 | mod class; 2 | mod function; 3 | mod method; 4 | mod parameters; 5 | mod r#type; 6 | 7 | pub use class::ReflectionClass; 8 | pub use function::{ReflectionFunction, ReflectionFunctionLike}; 9 | pub use method::ReflectionMethod; 10 | pub use parameters::{ReflectionParameter, ReflectsParameters}; 11 | pub use r#type::ReflectionType; 12 | -------------------------------------------------------------------------------- /crates/index/src/reflection/parameters.rs: -------------------------------------------------------------------------------- 1 | use pxp_bytestring::ByteStr; 2 | 3 | use crate::{ 4 | location::{HasLocation, Location}, 5 | Parameter, 6 | }; 7 | 8 | use super::ReflectionType; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | pub struct ReflectionParameter<'a, O: CanReflectParameters> { 12 | entity: &'a Parameter, 13 | owner: O, 14 | } 15 | 16 | impl<'a, O: CanReflectParameters> ReflectionParameter<'a, O> { 17 | pub fn new(entity: &'a Parameter, owner: O) -> Self { 18 | Self { entity, owner } 19 | } 20 | 21 | pub fn get_name(&self) -> &ByteStr { 22 | self.entity.name.stripped.as_ref() 23 | } 24 | 25 | pub fn has_type(&self) -> bool { 26 | self.entity.r#type.is_some() 27 | } 28 | 29 | pub fn get_type(&self) -> Option> { 30 | self.entity.r#type.as_ref().map(|t| ReflectionType::new(t)) 31 | } 32 | 33 | pub fn is_optional(&self) -> bool { 34 | self.entity.optional 35 | } 36 | 37 | pub fn is_variadic(&self) -> bool { 38 | todo!() 39 | } 40 | } 41 | 42 | impl<'a, O: CanReflectParameters> HasLocation for ReflectionParameter<'a, O> { 43 | fn location(&self) -> Location { 44 | self.entity.location 45 | } 46 | } 47 | 48 | pub trait CanReflectParameters {} 49 | 50 | pub trait ReflectsParameters<'a, O: CanReflectParameters>: CanReflectParameters { 51 | fn get_parameters(&self) -> Vec>; 52 | 53 | fn get_number_of_parameters(&self) -> usize { 54 | self.get_parameters().len() 55 | } 56 | 57 | fn get_number_of_required_parameters(&self) -> usize { 58 | self.get_parameters() 59 | .iter() 60 | .filter(|p| !p.is_optional()) 61 | .count() 62 | } 63 | 64 | fn is_variadic(&self) -> bool { 65 | self.get_parameters().iter().any(|p| p.is_variadic()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/index/src/reflection/type.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::ResolvedName; 2 | use pxp_type::Type; 3 | 4 | pub struct ReflectionType<'a> { 5 | entity: &'a Type, 6 | } 7 | 8 | impl<'a> ReflectionType<'a> { 9 | pub fn new(entity: &'a Type) -> Self { 10 | Self { entity } 11 | } 12 | 13 | pub fn allows_null(&self) -> bool { 14 | self.entity.allows_null() 15 | } 16 | 17 | pub fn is(&self, other: &Type) -> bool { 18 | self.entity == other 19 | } 20 | 21 | pub fn to_type(&self) -> &Type { 22 | self.entity 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/index/tests/fixtures/functions.php: -------------------------------------------------------------------------------- 1 | Index { 56 | let mut index = Index::new(); 57 | let files = discover(&["php"], &["./tests/fixtures"]).expect("Failed to load fixture files."); 58 | 59 | for file in files.iter() { 60 | index.index_file(&file); 61 | } 62 | 63 | index 64 | } 65 | -------------------------------------------------------------------------------- /crates/inference/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-inference" 3 | description = "Performs type inference on PHP code." 4 | version.workspace = true 5 | authors.workspace = true 6 | license-file.workspace = true 7 | rust-version.workspace = true 8 | edition.workspace = true 9 | 10 | [dependencies] 11 | pxp-ast = { version = "0.1.0", path = "../ast" } 12 | pxp-bytestring = { version = "0.1.0", path = "../bytestring" } 13 | pxp-index = { version = "0.1.0", path = "../index" } 14 | pxp-token = { version = "0.1.0", path = "../token" } 15 | pxp-type = { version = "0.1.0", path = "../type" } 16 | 17 | [dev-dependencies] 18 | pxp-lexer = { path = "../lexer" } 19 | pxp-node-finder = { path = "../node-finder" } 20 | pxp-parser = { path = "../parser" } 21 | -------------------------------------------------------------------------------- /crates/inference/src/map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use pxp_ast::{NodeId, ResolvedName}; 4 | use pxp_type::Type; 5 | 6 | #[derive(Debug, Default)] 7 | pub struct TypeMap { 8 | map: HashMap>, 9 | } 10 | 11 | /// A small wrapper around a dictionary that maps AST nodes to `Type` values based on their `NodeId`. 12 | /// 13 | /// Using the `NodeId` allows you to generate the type information once for the given AST and 14 | /// then use it across multiple passes without needing to regenerate or recalculate types. 15 | impl TypeMap { 16 | pub fn new() -> Self { 17 | Self::default() 18 | } 19 | 20 | /// Insert a type for the given node. 21 | pub fn insert(&mut self, id: NodeId, ty: Type) { 22 | self.map.insert(id, ty); 23 | } 24 | 25 | /// Get the type for the given node. If no type is present in the map, then `Type::Mixed` is returned. 26 | pub fn resolve(&self, id: NodeId) -> &Type { 27 | self.map.get(&id).unwrap_or_else(|| &Type::Mixed) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/lexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-lexer" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-token = { path = "../token" } 11 | pxp-span = { path = "../span" } 12 | pxp-bytestring = { path = "../bytestring" } 13 | pxp-diagnostics = { version = "0.1.0", path = "../diagnostics" } 14 | 15 | -------------------------------------------------------------------------------- /crates/lexer/README.md: -------------------------------------------------------------------------------- 1 | # Lexer 2 | 3 | This crate contains all of the code required to convert a string of PHP or PXP code into a set of tokens. 4 | 5 | ## Overview 6 | 7 | This code takes in a string of PHP or PXP code and produces a list of named tokens. 8 | 9 | It differs from PHP's own lexer / tokeniser due to the fact that it doesn't use the same token names or IDs and actually has a more extensive list of tokens for things such as `true`, `false`, `null`, `self`, `parent`, etc. 10 | 11 | For more information about the tokens themselves, consult the [Token](/crates/token) crate. 12 | 13 | ## Performance 14 | 15 | Rudimentary benchmarks suggest that this crate is ~40% faster than PHP's internal lexer, executed using the `token_get_all()` function or `PhpToken::tokenize()` method. 16 | 17 | This benchmark isn't incredibly fair, since our lexer doesn't produce an identical set of tokens, but the performance difference is still good. -------------------------------------------------------------------------------- /crates/lexer/src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | use pxp_diagnostics::DiagnosticKind; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum LexerDiagnostic { 5 | UnexpectedEndOfFile, 6 | UnexpectedCharacter(u8), 7 | InvalidHaltCompiler, 8 | InvalidUnicodeEscapeSequence, 9 | InvalidOctalSequence, 10 | } 11 | 12 | impl DiagnosticKind for LexerDiagnostic { 13 | fn get_code(&self) -> String { 14 | String::from(match self { 15 | Self::UnexpectedEndOfFile => "L001", 16 | Self::UnexpectedCharacter(_) => "L002", 17 | Self::InvalidHaltCompiler => "L003", 18 | Self::InvalidUnicodeEscapeSequence => "L004", 19 | Self::InvalidOctalSequence => "L005", 20 | }) 21 | } 22 | 23 | fn get_identifier(&self) -> String { 24 | String::from(match self { 25 | Self::UnexpectedEndOfFile => "lexer.unexpected-end-of-file", 26 | Self::UnexpectedCharacter(_) => "lexer.unexpected-character", 27 | Self::InvalidHaltCompiler => "lexer.invalid-halt-compiler", 28 | Self::InvalidUnicodeEscapeSequence => "lexer.invalid-unicode-escape-sequence", 29 | Self::InvalidOctalSequence => "lexer.invalid-octal-escape-sequence", 30 | }) 31 | } 32 | 33 | fn get_message(&self) -> String { 34 | String::from(match self { 35 | Self::UnexpectedEndOfFile => "unexpected end of file", 36 | Self::UnexpectedCharacter(_) => "unexpected character", 37 | Self::InvalidHaltCompiler => "invalid halt compiler directive", 38 | Self::InvalidUnicodeEscapeSequence => "invalid unicode escape sequence", 39 | Self::InvalidOctalSequence => "invalid octal escape sequence", 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/lexer/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! ident_start { 3 | () => { 4 | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\x80'..=b'\xff' 5 | }; 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! ident { 10 | () => { 11 | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\x80'..=b'\xff' 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /crates/node-finder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-node-finder" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-ast = { version = "0.1.0", path = "../ast" } 11 | pxp-span = { version = "0.1.0", path = "../span" } 12 | 13 | [dev-dependencies] 14 | pxp-lexer = { path = "../lexer" } 15 | pxp-parser = { path = "../parser" } 16 | -------------------------------------------------------------------------------- /crates/node-finder/src/lib.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::{ 2 | visitor::{Ancestors, NodeVisitor, NodeVisitorEscapeHatch}, 3 | Node, Statement, 4 | }; 5 | use pxp_span::ByteOffset; 6 | 7 | pub struct NodeFinder<'a> { 8 | offset: ByteOffset, 9 | found: Option<(Node<'a>, Ancestors<'a>)>, 10 | } 11 | 12 | impl<'a> NodeFinder<'a> { 13 | pub fn find_at_byte_offset( 14 | ast: &'a [Statement], 15 | offset: ByteOffset, 16 | ) -> Option<(Node<'a>, Ancestors<'a>)> { 17 | let mut finder = NodeFinder { 18 | offset, 19 | found: None, 20 | }; 21 | 22 | finder.traverse(ast); 23 | finder.found 24 | } 25 | } 26 | 27 | impl<'a> NodeVisitor<'a> for NodeFinder<'a> { 28 | fn enter(&mut self, node: Node<'a>, ancestors: &mut Ancestors<'a>) -> NodeVisitorEscapeHatch { 29 | let span = node.span; 30 | 31 | // If the current node is before the offset we're interested in, 32 | // there's no need to iterate through its children. 33 | if span.is_before_offset(self.offset) { 34 | return NodeVisitorEscapeHatch::SkipChildren; 35 | } 36 | 37 | // If we're looking at a node that comes after the offset we're interested in, 38 | // we can stop traversing the AST since we should have found the node we're looking for. 39 | if span.is_after_offset(self.offset) { 40 | return NodeVisitorEscapeHatch::Stop; 41 | } 42 | 43 | // If the current node contains the offset we're interested in, 44 | // we should keep track of it and continue traversing the AST. 45 | if span.contains_offset(self.offset) { 46 | self.found = Some((node.clone(), ancestors.clone())); 47 | } 48 | 49 | NodeVisitorEscapeHatch::Continue 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use pxp_ast::ExpressionKind; 56 | use pxp_lexer::Lexer; 57 | use pxp_parser::{ParseResult, Parser}; 58 | 59 | use super::*; 60 | 61 | #[test] 62 | fn it_can_find_a_node_at_offset() { 63 | let (result, offset) = parse_with_offset_indicator( 64 | r#" 65 | § 68 | "#, 69 | ); 70 | 71 | let (node, _) = NodeFinder::find_at_byte_offset(&result.ast[..], offset).unwrap(); 72 | 73 | assert!(node.is_property_fetch_expression()); 74 | 75 | let property_fetch = node.as_property_fetch_expression().unwrap(); 76 | 77 | assert!(matches!( 78 | property_fetch.target.kind, 79 | ExpressionKind::Parenthesized(_) 80 | )); 81 | } 82 | 83 | fn parse_with_offset_indicator(input: &'static str) -> (ParseResult, ByteOffset) { 84 | let offset = input.find('§').unwrap(); 85 | let input = input.replace('§', ""); 86 | let result = Parser::parse(Lexer::new(&input)); 87 | 88 | (result, offset) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-parser" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-span = { path = "../span" } 11 | pxp-bytestring = { path = "../bytestring" } 12 | pxp-token = { path = "../token" } 13 | pxp-ast = { path = "../ast" } 14 | pxp-lexer = { path = "../lexer" } 15 | pxp-type = { path = "../type" } 16 | pxp-diagnostics = { path = "../diagnostics" } 17 | 18 | [dev-dependencies] 19 | snappers = { path = "../snappers" } 20 | -------------------------------------------------------------------------------- /crates/parser/README.md: -------------------------------------------------------------------------------- 1 | # Parser 2 | 3 | This crate contains all of the code required to convert a stream of tokens into a set of AST nodes. 4 | 5 | ## Overview 6 | 7 | The parser takes in a stream of tokens and translates recognisable patterns into AST nodes. It implements a recursive-descent style parser with precedence and associativity rules for parsing expressions. 8 | 9 | ### Error tolerance 10 | 11 | One of the goals of the parser is to be error-tolerant so that it can be used inside of the language server project. The approach to error tolerance is heavily inspired by the excellent [`microsoft/tolerant-php-parser`](https://github.com/microsoft/tolerant-php-parser) package. 12 | 13 | Error tolerance support is entirely subjective and will improve over time. The majority of places it is supported is solely based on language server development and the most common places where tolerance is required for autocomplete. 14 | 15 | ### Performance 16 | 17 | Rudimentary benchmarks show that this parser is ~8-9 times faster than the most popular PHP parser [nikic/php-parser](https://github.com/nikic/PHP-Parser). 18 | 19 | > This result is of course expected given that `nikic/php-parser` is written purely in PHP and uses a YACC-style parser generator, whereas the parser provided by this project is handwritten and uses Rust. 20 | 21 | When compared to PHP's own parser (exposed via `ext-ast`), it is about 30% slower which equates to about 400ms. There are definitely parsing-specific optimisations to be made in the PXP parser, but our parser also does more work such as resolving names while parsing. -------------------------------------------------------------------------------- /crates/parser/src/internal/attributes.rs: -------------------------------------------------------------------------------- 1 | use crate::Parser; 2 | use pxp_ast::*; 3 | use pxp_span::Span; 4 | use pxp_token::TokenKind; 5 | 6 | impl<'a> Parser<'a> { 7 | pub(crate) fn get_attributes(&mut self) -> Vec { 8 | let mut attributes = vec![]; 9 | 10 | std::mem::swap(&mut self.attributes, &mut attributes); 11 | 12 | attributes 13 | } 14 | 15 | pub(crate) fn attribute(&mut self, attr: AttributeGroup) { 16 | self.attributes.push(attr); 17 | } 18 | 19 | pub(crate) fn gather_attributes(&mut self) -> bool { 20 | if self.current_kind() != TokenKind::Attribute { 21 | return false; 22 | } 23 | 24 | let start = self.current_span(); 25 | let mut members = vec![]; 26 | 27 | self.next(); 28 | 29 | loop { 30 | let start = self.current_span(); 31 | let name = self.parse_full_name_including_self(); 32 | let arguments = if self.current_kind() == TokenKind::LeftParen { 33 | Some(self.parse_argument_list()) 34 | } else { 35 | None 36 | }; 37 | let end = self.current_span(); 38 | let span = Span::new(start.start, end.end); 39 | 40 | members.push(Attribute { 41 | id: self.id(), 42 | span, 43 | name, 44 | arguments, 45 | }); 46 | 47 | if self.current_kind() == TokenKind::Comma { 48 | self.next(); 49 | 50 | if self.current_kind() == TokenKind::RightBracket { 51 | break; 52 | } 53 | 54 | continue; 55 | } 56 | 57 | break; 58 | } 59 | 60 | let end = self.skip_right_bracket(); 61 | let span = Span::new(start.start, end.end); 62 | 63 | let id = self.id(); 64 | 65 | self.attribute(AttributeGroup { id, span, members }); 66 | self.gather_attributes() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/parser/src/internal/blocks.rs: -------------------------------------------------------------------------------- 1 | use crate::Parser; 2 | use pxp_ast::BlockStatement; 3 | use pxp_ast::Statement; 4 | use pxp_ast::StatementKind; 5 | use pxp_span::Span; 6 | use pxp_token::OpenTagKind; 7 | use pxp_token::TokenKind; 8 | 9 | impl<'a> Parser<'a> { 10 | pub fn parse_block_statement(&mut self) -> StatementKind { 11 | let (left_brace, statements, right_brace) = 12 | self.braced(|parser| parser.parse_multiple_statements_until(TokenKind::RightBrace)); 13 | 14 | StatementKind::Block(Box::new(BlockStatement { 15 | id: self.id(), 16 | span: Span::combine(left_brace, right_brace), 17 | left_brace, 18 | statements, 19 | right_brace, 20 | })) 21 | } 22 | 23 | pub fn parse_multiple_statements_until(&mut self, until: TokenKind) -> Vec { 24 | let mut statements = Vec::new(); 25 | 26 | while !self.is_eof() && self.current_kind() != until { 27 | if let TokenKind::OpenTag(OpenTagKind::Full) = self.current_kind() { 28 | self.next(); 29 | 30 | continue; 31 | } 32 | 33 | statements.push(self.parse_statement()); 34 | } 35 | 36 | statements 37 | } 38 | 39 | pub fn parse_multiple_statements_until_any(&mut self, until: &[TokenKind]) -> Vec { 40 | let mut statements = Vec::new(); 41 | 42 | while !until.contains(&self.current_kind()) { 43 | if let TokenKind::OpenTag(OpenTagKind::Full) = self.current_kind() { 44 | self.next(); 45 | 46 | continue; 47 | } 48 | 49 | statements.push(self.parse_statement()); 50 | } 51 | 52 | statements 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/parser/src/internal/goto.rs: -------------------------------------------------------------------------------- 1 | use crate::Parser; 2 | use pxp_ast::StatementKind; 3 | use pxp_ast::*; 4 | use pxp_span::Span; 5 | use pxp_token::TokenKind; 6 | 7 | impl<'a> Parser<'a> { 8 | pub fn parse_label_statement(&mut self) -> StatementKind { 9 | let comments = self.comments(); 10 | let label = self.parse_label_identifier(); 11 | let colon = self.skip_colon(); 12 | 13 | StatementKind::Label(Box::new(LabelStatement { 14 | id: self.id(), 15 | span: Span::combine(label.span, colon), 16 | comments, 17 | label, 18 | colon, 19 | })) 20 | } 21 | 22 | pub fn parse_goto_statement(&mut self) -> StatementKind { 23 | let comments = self.comments(); 24 | let keyword = self.skip(TokenKind::Goto); 25 | let label = self.parse_label_identifier(); 26 | let semicolon = self.skip_semicolon(); 27 | 28 | StatementKind::Goto(Box::new(GotoStatement { 29 | id: self.id(), 30 | span: Span::combine(keyword, semicolon), 31 | comments, 32 | keyword, 33 | label, 34 | semicolon, 35 | })) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/parser/src/internal/literals.rs: -------------------------------------------------------------------------------- 1 | use pxp_ast::*; 2 | use pxp_diagnostics::Severity; 3 | use pxp_token::TokenKind; 4 | 5 | use crate::{Parser, ParserDiagnostic}; 6 | 7 | impl<'a> Parser<'a> { 8 | pub fn parse_literal(&mut self) -> Literal { 9 | let token = self.current().to_owned(); 10 | let span = self.current_span(); 11 | let kind = match self.current_kind() { 12 | TokenKind::LiteralInteger => self.next_but_first(|_| LiteralKind::Integer), 13 | TokenKind::LiteralFloat => self.next_but_first(|_| LiteralKind::Float), 14 | TokenKind::LiteralSingleQuotedString | TokenKind::LiteralDoubleQuotedString => { 15 | self.next_but_first(|_| LiteralKind::String) 16 | } 17 | _ => { 18 | self.diagnostic( 19 | ParserDiagnostic::ExpectedToken { 20 | expected: vec![ 21 | TokenKind::LiteralInteger, 22 | TokenKind::LiteralFloat, 23 | TokenKind::LiteralSingleQuotedString, 24 | TokenKind::LiteralDoubleQuotedString, 25 | ], 26 | found: token, 27 | }, 28 | Severity::Error, 29 | span, 30 | ); 31 | 32 | return Literal::missing(self.id(), span); 33 | } 34 | }; 35 | 36 | Literal { 37 | id: self.id(), 38 | span, 39 | kind, 40 | token, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/parser/src/internal/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod arrays; 2 | pub(crate) mod attributes; 3 | pub(crate) mod blocks; 4 | pub(crate) mod classes; 5 | pub(crate) mod comments; 6 | pub(crate) mod constants; 7 | pub(crate) mod control_flow; 8 | pub(crate) mod data_type; 9 | pub(crate) mod diagnostics; 10 | pub(crate) mod docblock; 11 | pub(crate) mod enums; 12 | pub(crate) mod expressions; 13 | pub(crate) mod functions; 14 | pub(crate) mod goto; 15 | pub(crate) mod identifiers; 16 | pub(crate) mod imports; 17 | pub(crate) mod interfaces; 18 | pub(crate) mod literals; 19 | pub(crate) mod loops; 20 | pub(crate) mod modifiers; 21 | pub(crate) mod names; 22 | pub(crate) mod namespaces; 23 | pub(crate) mod parameters; 24 | pub(crate) mod precedences; 25 | pub(crate) mod properties; 26 | pub(crate) mod statement; 27 | pub(crate) mod strings; 28 | pub(crate) mod traits; 29 | pub(crate) mod try_block; 30 | pub(crate) mod uses; 31 | pub(crate) mod utils; 32 | pub(crate) mod variables; 33 | -------------------------------------------------------------------------------- /crates/parser/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! scoped { 3 | ($state:expr, $scope:expr, $block:block) => {{ 4 | $state.enter($scope); 5 | 6 | let result = $block; 7 | 8 | $state.exit(); 9 | 10 | result 11 | }}; 12 | } 13 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/abstract_class.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 9, 24 | kind: Class( 25 | ClassStatement { 26 | id: 8, 27 | span: Span { 28 | start: 7, 29 | end: 28, 30 | }, 31 | attributes: [], 32 | modifiers: ClassModifierGroup { 33 | id: 5, 34 | span: Span { 35 | start: 7, 36 | end: 15, 37 | }, 38 | modifiers: [ 39 | Abstract( 40 | Span { 41 | start: 7, 42 | end: 15, 43 | }, 44 | ), 45 | ], 46 | }, 47 | class: Span { 48 | start: 16, 49 | end: 21, 50 | }, 51 | name: Name { 52 | id: 6, 53 | kind: Resolved( 54 | ResolvedName { 55 | resolved: "Foo", 56 | original: "Foo", 57 | }, 58 | ), 59 | span: Span { 60 | start: 22, 61 | end: 25, 62 | }, 63 | }, 64 | extends: None, 65 | implements: None, 66 | body: ClassBody { 67 | id: 7, 68 | span: Span { 69 | start: 26, 70 | end: 28, 71 | }, 72 | left_brace: Span { 73 | start: 26, 74 | end: 27, 75 | }, 76 | members: [], 77 | right_brace: Span { 78 | start: 27, 79 | end: 28, 80 | }, 81 | }, 82 | }, 83 | ), 84 | span: Span { 85 | start: 7, 86 | end: 28, 87 | }, 88 | comments: CommentGroup { 89 | id: 4, 90 | comments: [], 91 | }, 92 | }, 93 | ] 94 | --- 95 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/backed_enum_int.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: BackedEnum( 25 | BackedEnumStatement { 26 | id: 7, 27 | span: Span { 28 | start: 7, 29 | end: 23, 30 | }, 31 | attributes: [], 32 | enum: Span { 33 | start: 7, 34 | end: 11, 35 | }, 36 | name: Name { 37 | id: 5, 38 | kind: Resolved( 39 | ResolvedName { 40 | resolved: "Foo", 41 | original: "Foo", 42 | }, 43 | ), 44 | span: Span { 45 | start: 12, 46 | end: 15, 47 | }, 48 | }, 49 | colon: Span { 50 | start: 15, 51 | end: 16, 52 | }, 53 | backed_type: Int( 54 | Span { 55 | start: 17, 56 | end: 20, 57 | }, 58 | ), 59 | implements: [], 60 | body: BackedEnumBody { 61 | id: 6, 62 | span: Span { 63 | start: 21, 64 | end: 23, 65 | }, 66 | left_brace: Span { 67 | start: 21, 68 | end: 22, 69 | }, 70 | members: [], 71 | right_brace: Span { 72 | start: 22, 73 | end: 23, 74 | }, 75 | }, 76 | }, 77 | ), 78 | span: Span { 79 | start: 7, 80 | end: 23, 81 | }, 82 | comments: CommentGroup { 83 | id: 4, 84 | comments: [], 85 | }, 86 | }, 87 | ] 88 | --- 89 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/backed_enum_string.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: BackedEnum( 25 | BackedEnumStatement { 26 | id: 7, 27 | span: Span { 28 | start: 7, 29 | end: 26, 30 | }, 31 | attributes: [], 32 | enum: Span { 33 | start: 7, 34 | end: 11, 35 | }, 36 | name: Name { 37 | id: 5, 38 | kind: Resolved( 39 | ResolvedName { 40 | resolved: "Foo", 41 | original: "Foo", 42 | }, 43 | ), 44 | span: Span { 45 | start: 12, 46 | end: 15, 47 | }, 48 | }, 49 | colon: Span { 50 | start: 15, 51 | end: 16, 52 | }, 53 | backed_type: String( 54 | Span { 55 | start: 17, 56 | end: 23, 57 | }, 58 | ), 59 | implements: [], 60 | body: BackedEnumBody { 61 | id: 6, 62 | span: Span { 63 | start: 24, 64 | end: 26, 65 | }, 66 | left_brace: Span { 67 | start: 24, 68 | end: 25, 69 | }, 70 | members: [], 71 | right_brace: Span { 72 | start: 25, 73 | end: 26, 74 | }, 75 | }, 76 | }, 77 | ), 78 | span: Span { 79 | start: 7, 80 | end: 26, 81 | }, 82 | comments: CommentGroup { 83 | id: 4, 84 | comments: [], 85 | }, 86 | }, 87 | ] 88 | --- 89 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/braced_namespace.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: Namespace( 25 | Braced( 26 | BracedNamespace { 27 | id: 7, 28 | span: Span { 29 | start: 7, 30 | end: 29, 31 | }, 32 | namespace: Span { 33 | start: 7, 34 | end: 16, 35 | }, 36 | name: Some( 37 | SimpleIdentifier { 38 | id: 5, 39 | symbol: "Foo", 40 | span: Span { 41 | start: 17, 42 | end: 20, 43 | }, 44 | }, 45 | ), 46 | body: BracedNamespaceBody { 47 | id: 6, 48 | span: Span { 49 | start: 21, 50 | end: 29, 51 | }, 52 | start: Span { 53 | start: 21, 54 | end: 22, 55 | }, 56 | end: Span { 57 | start: 28, 58 | end: 29, 59 | }, 60 | statements: [], 61 | }, 62 | }, 63 | ), 64 | ), 65 | span: Span { 66 | start: 7, 67 | end: 29, 68 | }, 69 | comments: CommentGroup { 70 | id: 4, 71 | comments: [], 72 | }, 73 | }, 74 | ] 75 | --- 76 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/echo_missing_semicolon.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: Echo( 25 | EchoStatement { 26 | id: 7, 27 | span: Span { 28 | start: 7, 29 | end: 13, 30 | }, 31 | echo: Span { 32 | start: 7, 33 | end: 11, 34 | }, 35 | values: [ 36 | Expression { 37 | id: 5, 38 | kind: Literal( 39 | Literal { 40 | id: 6, 41 | span: Span { 42 | start: 13, 43 | end: 25, 44 | }, 45 | kind: String, 46 | token: OwnedToken { 47 | kind: LiteralDoubleQuotedString, 48 | span: Span { 49 | start: 13, 50 | end: 25, 51 | }, 52 | symbol: "Hello, world", 53 | }, 54 | }, 55 | ), 56 | span: Span { 57 | start: 13, 58 | end: 25, 59 | }, 60 | comments: CommentGroup { 61 | id: 0, 62 | comments: [], 63 | }, 64 | }, 65 | ], 66 | ending: Missing( 67 | Span { 68 | start: 13, 69 | end: 13, 70 | }, 71 | ), 72 | }, 73 | ), 74 | span: Span { 75 | start: 7, 76 | end: 13, 77 | }, 78 | comments: CommentGroup { 79 | id: 4, 80 | comments: [], 81 | }, 82 | }, 83 | ] 84 | --- 85 | [ 86 | Diagnostic { 87 | kind: UnexpectedEndOfFile, 88 | severity: Error, 89 | span: Span { 90 | start: 13, 91 | end: 13, 92 | }, 93 | }, 94 | ] -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/echo_tag.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: EchoOpeningTag( 5 | EchoOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 3, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 3, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 6, 24 | kind: ClosingTag( 25 | ClosingTagStatement { 26 | id: 5, 27 | span: Span { 28 | start: 5, 29 | end: 7, 30 | }, 31 | }, 32 | ), 33 | span: Span { 34 | start: 5, 35 | end: 7, 36 | }, 37 | comments: CommentGroup { 38 | id: 4, 39 | comments: [], 40 | }, 41 | }, 42 | ] 43 | --- 44 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/empty_file.snap: -------------------------------------------------------------------------------- 1 | [] 2 | --- 3 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/empty_heredoc.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 9, 24 | kind: Expression( 25 | ExpressionStatement { 26 | id: 8, 27 | span: Span { 28 | start: 7, 29 | end: 19, 30 | }, 31 | expression: Expression { 32 | id: 6, 33 | kind: Heredoc( 34 | HeredocExpression { 35 | id: 7, 36 | span: Span { 37 | start: 7, 38 | end: 18, 39 | }, 40 | label: "<<Hello, world!\n

Goodbye, world!

", 18 | }, 19 | }, 20 | ), 21 | span: Span { 22 | start: 0, 23 | end: 47, 24 | }, 25 | comments: CommentGroup { 26 | id: 1, 27 | comments: [], 28 | }, 29 | }, 30 | ] 31 | --- 32 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/null.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 7, 24 | kind: Expression( 25 | ExpressionStatement { 26 | id: 6, 27 | span: Span { 28 | start: 7, 29 | end: 12, 30 | }, 31 | expression: Expression { 32 | id: 5, 33 | kind: Null( 34 | Span { 35 | start: 7, 36 | end: 11, 37 | }, 38 | ), 39 | span: Span { 40 | start: 7, 41 | end: 11, 42 | }, 43 | comments: CommentGroup { 44 | id: 0, 45 | comments: [], 46 | }, 47 | }, 48 | ending: Semicolon( 49 | Span { 50 | start: 11, 51 | end: 12, 52 | }, 53 | ), 54 | }, 55 | ), 56 | span: Span { 57 | start: 7, 58 | end: 12, 59 | }, 60 | comments: CommentGroup { 61 | id: 4, 62 | comments: [], 63 | }, 64 | }, 65 | ] 66 | --- 67 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/readonly_class.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 9, 24 | kind: Class( 25 | ClassStatement { 26 | id: 8, 27 | span: Span { 28 | start: 7, 29 | end: 28, 30 | }, 31 | attributes: [], 32 | modifiers: ClassModifierGroup { 33 | id: 5, 34 | span: Span { 35 | start: 7, 36 | end: 15, 37 | }, 38 | modifiers: [ 39 | Readonly( 40 | Span { 41 | start: 7, 42 | end: 15, 43 | }, 44 | ), 45 | ], 46 | }, 47 | class: Span { 48 | start: 16, 49 | end: 21, 50 | }, 51 | name: Name { 52 | id: 6, 53 | kind: Resolved( 54 | ResolvedName { 55 | resolved: "Foo", 56 | original: "Foo", 57 | }, 58 | ), 59 | span: Span { 60 | start: 22, 61 | end: 25, 62 | }, 63 | }, 64 | extends: None, 65 | implements: None, 66 | body: ClassBody { 67 | id: 7, 68 | span: Span { 69 | start: 26, 70 | end: 28, 71 | }, 72 | left_brace: Span { 73 | start: 26, 74 | end: 27, 75 | }, 76 | members: [], 77 | right_brace: Span { 78 | start: 27, 79 | end: 28, 80 | }, 81 | }, 82 | }, 83 | ), 84 | span: Span { 85 | start: 7, 86 | end: 28, 87 | }, 88 | comments: CommentGroup { 89 | id: 4, 90 | comments: [], 91 | }, 92 | }, 93 | ] 94 | --- 95 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/short_tag.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: ShortOpeningTag( 5 | ShortOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 2, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 2, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | ] 23 | --- 24 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/simple_class.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 9, 24 | kind: Class( 25 | ClassStatement { 26 | id: 8, 27 | span: Span { 28 | start: 7, 29 | end: 25, 30 | }, 31 | attributes: [], 32 | modifiers: ClassModifierGroup { 33 | id: 5, 34 | span: Span { 35 | start: 0, 36 | end: 0, 37 | }, 38 | modifiers: [], 39 | }, 40 | class: Span { 41 | start: 7, 42 | end: 12, 43 | }, 44 | name: Name { 45 | id: 6, 46 | kind: Resolved( 47 | ResolvedName { 48 | resolved: "Foo", 49 | original: "Foo", 50 | }, 51 | ), 52 | span: Span { 53 | start: 13, 54 | end: 16, 55 | }, 56 | }, 57 | extends: None, 58 | implements: None, 59 | body: ClassBody { 60 | id: 7, 61 | span: Span { 62 | start: 17, 63 | end: 25, 64 | }, 65 | left_brace: Span { 66 | start: 17, 67 | end: 18, 68 | }, 69 | members: [], 70 | right_brace: Span { 71 | start: 24, 72 | end: 25, 73 | }, 74 | }, 75 | }, 76 | ), 77 | span: Span { 78 | start: 7, 79 | end: 25, 80 | }, 81 | comments: CommentGroup { 82 | id: 4, 83 | comments: [], 84 | }, 85 | }, 86 | ] 87 | --- 88 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/simple_echo.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: Echo( 25 | EchoStatement { 26 | id: 7, 27 | span: Span { 28 | start: 7, 29 | end: 28, 30 | }, 31 | echo: Span { 32 | start: 7, 33 | end: 11, 34 | }, 35 | values: [ 36 | Expression { 37 | id: 5, 38 | kind: Literal( 39 | Literal { 40 | id: 6, 41 | span: Span { 42 | start: 13, 43 | end: 26, 44 | }, 45 | kind: String, 46 | token: OwnedToken { 47 | kind: LiteralDoubleQuotedString, 48 | span: Span { 49 | start: 13, 50 | end: 26, 51 | }, 52 | symbol: "Hello, world!", 53 | }, 54 | }, 55 | ), 56 | span: Span { 57 | start: 13, 58 | end: 26, 59 | }, 60 | comments: CommentGroup { 61 | id: 0, 62 | comments: [], 63 | }, 64 | }, 65 | ], 66 | ending: Semicolon( 67 | Span { 68 | start: 27, 69 | end: 28, 70 | }, 71 | ), 72 | }, 73 | ), 74 | span: Span { 75 | start: 7, 76 | end: 28, 77 | }, 78 | comments: CommentGroup { 79 | id: 4, 80 | comments: [], 81 | }, 82 | }, 83 | ] 84 | --- 85 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/simple_enum.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 8, 24 | kind: UnitEnum( 25 | UnitEnumStatement { 26 | id: 7, 27 | span: Span { 28 | start: 7, 29 | end: 18, 30 | }, 31 | attributes: [], 32 | enum: Span { 33 | start: 7, 34 | end: 11, 35 | }, 36 | name: Name { 37 | id: 5, 38 | kind: Resolved( 39 | ResolvedName { 40 | resolved: "Foo", 41 | original: "Foo", 42 | }, 43 | ), 44 | span: Span { 45 | start: 12, 46 | end: 15, 47 | }, 48 | }, 49 | implements: [], 50 | body: UnitEnumBody { 51 | id: 6, 52 | span: Span { 53 | start: 16, 54 | end: 18, 55 | }, 56 | left_brace: Span { 57 | start: 16, 58 | end: 17, 59 | }, 60 | members: [], 61 | right_brace: Span { 62 | start: 17, 63 | end: 18, 64 | }, 65 | }, 66 | }, 67 | ), 68 | span: Span { 69 | start: 7, 70 | end: 18, 71 | }, 72 | comments: CommentGroup { 73 | id: 4, 74 | comments: [], 75 | }, 76 | }, 77 | ] 78 | --- 79 | -------------------------------------------------------------------------------- /crates/parser/tests/__snapshots__/simple_heredoc.snap: -------------------------------------------------------------------------------- 1 | [ 2 | Statement { 3 | id: 3, 4 | kind: FullOpeningTag( 5 | FullOpeningTagStatement { 6 | id: 2, 7 | span: Span { 8 | start: 0, 9 | end: 5, 10 | }, 11 | }, 12 | ), 13 | span: Span { 14 | start: 0, 15 | end: 5, 16 | }, 17 | comments: CommentGroup { 18 | id: 1, 19 | comments: [], 20 | }, 21 | }, 22 | Statement { 23 | id: 9, 24 | kind: Expression( 25 | ExpressionStatement { 26 | id: 8, 27 | span: Span { 28 | start: 7, 29 | end: 59, 30 | }, 31 | expression: Expression { 32 | id: 6, 33 | kind: Heredoc( 34 | HeredocExpression { 35 | id: 7, 36 | span: Span { 37 | start: 7, 38 | end: 58, 39 | }, 40 | label: "<<>= 1; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/assignments/bitwise-xor-assign.php: -------------------------------------------------------------------------------- 1 | > 1; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/bitwise/bitwise-xor.php: -------------------------------------------------------------------------------- 1 | b; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/clone/clone-var.php: -------------------------------------------------------------------------------- 1 | = $b; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/comparison/greater-than.php: -------------------------------------------------------------------------------- 1 | $b; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/comparison/identical.php: -------------------------------------------------------------------------------- 1 | $b; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/constants/constant.php: -------------------------------------------------------------------------------- 1 | $value) { 4 | // 5 | } -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/control/foreach-statement.php: -------------------------------------------------------------------------------- 1 | 'one', 5 | }; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/control/match-expression.php: -------------------------------------------------------------------------------- 1 | 'one', 5 | 2, 3 => 'two or three', 6 | default => 'other', 7 | }; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/control/switch-statement-no-case.php: -------------------------------------------------------------------------------- 1 | } */ 4 | $items = []; 5 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/array-shape-unsealed.php: -------------------------------------------------------------------------------- 1 | } */ 4 | $items = []; 5 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/array-shape-variadic.php: -------------------------------------------------------------------------------- 1 | (T[] $values) */ 4 | class A {} 5 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/param-dnf-type.php: -------------------------------------------------------------------------------- 1 | $a */ 4 | function a($a) {} -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/param-nested-generic-type.php: -------------------------------------------------------------------------------- 1 | > $a */ 4 | function a($a) {} -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/param-nested-typed-array.php: -------------------------------------------------------------------------------- 1 | $a */ 4 | function a($a) {} -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docblocks/param-simple-typed-array.php: -------------------------------------------------------------------------------- 1 | name}. 7 | Hello, {$world->name()}. 8 | Hello, {$world[0]}. 9 | TXT; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/docstrings/simple-heredoc.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/identifiers/fully-qualified-identifier.php: -------------------------------------------------------------------------------- 1 | name}"; 8 | "Hello, $user->name"; 9 | "Hello, {$user->getName()}"; -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/logical/and.php: -------------------------------------------------------------------------------- 1 | "Hello, world!"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/property-hooks/parameter-list.php: -------------------------------------------------------------------------------- 1 | a = $value; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/tags/echo-tag.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/tags/empty-file.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryangjchandler/pxp/d63ec46c22c8d00e746de650a31288c0853e9ca4/crates/parser/tests/fixtures/tags/empty-file.php -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/tags/html.php: -------------------------------------------------------------------------------- 1 |

Hello, world!

2 |

Goodbye, world!

-------------------------------------------------------------------------------- /crates/parser/tests/fixtures/tags/short-tag.php: -------------------------------------------------------------------------------- 1 | 1; 4 | -------------------------------------------------------------------------------- /crates/parser/tests/fixtures/yield/value.php: -------------------------------------------------------------------------------- 1 | Snapper { 13 | Snapper::new( 14 | // This is the path where generated snapshots will be stored. 15 | format!("{}/__snapshot__", env!("CARGO_MANIFEST_DIR")).into() 16 | ) 17 | } 18 | ``` 19 | 20 | Then generate test cases using the `snap!()` macro, passing through the name of the `snapper` function, the name of the test case (must be a valid Rust function name), as well as the test subject function. 21 | 22 | ```rs 23 | #[cfg(test)] 24 | mod tests { 25 | use snapper::{Snapper, snap}; 26 | 27 | snap!(snapper, it_can_say_hello_world, say_hello()); 28 | 29 | fn say_hello() -> String { 30 | return format!("Hello, world!"); 31 | } 32 | 33 | fn snapper() -> Snapper { 34 | Snapper::new( 35 | // This is the path where generated snapshots will be stored. 36 | format!("{}/__snapshot__", env!("CARGO_MANIFEST_DIR")).into() 37 | ) 38 | } 39 | } 40 | ``` 41 | 42 | Now when you run `cargo test`, the snapshot files will be generated and all further test runs will test against the generated snapshot file (if present). -------------------------------------------------------------------------------- /crates/snappers/__snapshots__/it_can_say_hello_world.snap: -------------------------------------------------------------------------------- 1 | Hello, world! -------------------------------------------------------------------------------- /crates/snappers/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[macro_export] 4 | macro_rules! snap { 5 | ($snapper:ident, $name:ident, $subject:expr) => { 6 | #[test] 7 | fn $name() { 8 | let snapper = $snapper(); 9 | let subject = $subject; 10 | let snapshot = snapper.snapshot_path(stringify!($name)); 11 | 12 | if !snapshot.exists() || snapper.should_regenerate_snapshots() { 13 | std::fs::create_dir_all(snapshot.parent().unwrap()).unwrap(); 14 | std::fs::write(&snapshot, subject.to_string()).unwrap(); 15 | 16 | println!("Snapshot created: {}", stringify!($name)); 17 | } else { 18 | let expected = std::fs::read_to_string(&snapshot).unwrap(); 19 | snapper.assert_eq(expected, format!("{}", subject)); 20 | } 21 | } 22 | }; 23 | } 24 | 25 | pub struct Snapper { 26 | directory: PathBuf, 27 | } 28 | 29 | impl Snapper { 30 | pub fn new(directory: PathBuf) -> Self { 31 | Self { directory } 32 | } 33 | 34 | /// Returns the path to a particular snapshot file: $CARGO_MANIFEST_DIR/__snapshots__/$name.snap 35 | pub fn snapshot_path(&self, name: &str) -> PathBuf { 36 | let mut path = self.directory.clone(); 37 | path.push(format!("{}.snap", name)); 38 | path 39 | } 40 | 41 | pub fn should_regenerate_snapshots(&self) -> bool { 42 | std::env::var("SNAPPERS_REGENERATE").is_ok() 43 | } 44 | 45 | pub fn assert_eq(&self, expected: String, actual: String) { 46 | pretty_assertions::assert_eq!(expected, actual); 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use crate::Snapper; 53 | 54 | use super::snap; 55 | 56 | snap!(snapper, it_can_say_hello_world, say_hello("world")); 57 | 58 | fn say_hello(name: &str) -> String { 59 | format!("Hello, {name}!") 60 | } 61 | 62 | fn snapper() -> Snapper { 63 | Snapper::new(format!("{}/__snapshots__", env!("CARGO_MANIFEST_DIR")).into()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/span/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-span" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | serde = { version = "1.0.193", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /crates/token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-token" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-bytestring = { path = "../bytestring" } 11 | pxp-span = { path = "../span" } 12 | -------------------------------------------------------------------------------- /crates/type/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pxp-type" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license-file.workspace = true 8 | 9 | [dependencies] 10 | pxp-bytestring = { version = "0.1.0", path = "../bytestring" } 11 | pxp-span = { path = "../span" } 12 | strum = { version = "0.26.3", features = ["derive"] } 13 | -------------------------------------------------------------------------------- /src/cmd/init.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use clap::Parser; 4 | use colored::Colorize; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about = "Initialise a new project.")] 8 | pub struct Init { 9 | #[arg(short, long, help = "Overwrite an existing configuration file.")] 10 | force: bool, 11 | } 12 | 13 | const STUB: &str = include_str!("../stubs/pxp.config.toml"); 14 | 15 | pub fn init(args: Init) -> anyhow::Result<()> { 16 | let cwd = std::env::current_dir()?; 17 | 18 | if cwd.join("pxp.config.toml").exists() && !args.force { 19 | anyhow::bail!( 20 | "Configuration file already exists. Use --force to overwrite existing configuration." 21 | ); 22 | } 23 | 24 | let paths = find_interesting_directories_in(&cwd)?; 25 | let stub = STUB.replace( 26 | r#""""#, 27 | &paths 28 | .iter() 29 | .map(|p| format!(r#""{}""#, p.display())) 30 | .collect::>() 31 | .join(",\n"), 32 | ); 33 | 34 | std::fs::write(cwd.join("pxp.config.toml"), stub)?; 35 | 36 | println!( 37 | "{}", 38 | "Configuration file created.".green().bold().underline() 39 | ); 40 | 41 | Ok(()) 42 | } 43 | 44 | fn find_interesting_directories_in(path: &Path) -> anyhow::Result> { 45 | let mut paths = Vec::new(); 46 | 47 | if path.join("src").exists() { 48 | paths.push(path.join("src").strip_prefix(path)?.to_path_buf()); 49 | } 50 | 51 | if path.join("lib").exists() { 52 | paths.push(path.join("lib").strip_prefix(path)?.to_path_buf()); 53 | } 54 | 55 | if path.join("artisan").exists() { 56 | paths.extend(vec![ 57 | path.join("app").strip_prefix(path)?.to_path_buf(), 58 | path.join("bootstrap").strip_prefix(path)?.to_path_buf(), 59 | path.join("config").strip_prefix(path)?.to_path_buf(), 60 | path.join("database").strip_prefix(path)?.to_path_buf(), 61 | path.join("routes").strip_prefix(path)?.to_path_buf(), 62 | path.join("tests").strip_prefix(path)?.to_path_buf(), 63 | ]); 64 | } 65 | 66 | if path.join("tests").exists() { 67 | paths.push(path.join("tests").strip_prefix(path)?.to_path_buf()); 68 | } 69 | 70 | paths.dedup(); 71 | Ok(paths) 72 | } 73 | -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | mod index; 2 | mod init; 3 | mod parse; 4 | mod tokenise; 5 | 6 | pub use index::{index, Index}; 7 | pub use init::{init, Init}; 8 | pub use parse::{parse, Parse}; 9 | pub use tokenise::{tokenise, Tokenise}; 10 | -------------------------------------------------------------------------------- /src/cmd/parse.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use clap::Parser as Args; 4 | use pxp_diagnostics::DiagnosticKind; 5 | use pxp_lexer::Lexer; 6 | use pxp_parser::Parser; 7 | use pxp_span::IsSpanned; 8 | 9 | use crate::utils::find_php_files_in; 10 | 11 | #[derive(Debug, Args)] 12 | #[command(version, about = "Parse a file or directory.")] 13 | pub struct Parse { 14 | #[arg(help = "The path to a file or directory.")] 15 | path: PathBuf, 16 | 17 | #[arg(short, long, help = "Dump the AST to stdout.")] 18 | dump: bool, 19 | 20 | #[arg(short = 'f', long, help = "Print filenames when parsing a directory.")] 21 | print_filenames: bool, 22 | 23 | #[arg(short, long, help = "Print diagnostics after parsing a file.")] 24 | print_diagnostics: bool, 25 | } 26 | 27 | pub fn parse(args: Parse) -> anyhow::Result<()> { 28 | let files = if args.path.is_dir() { 29 | find_php_files_in(&args.path)? 30 | } else { 31 | vec![args.path] 32 | }; 33 | 34 | for file in files { 35 | if args.print_filenames { 36 | println!("{}", file.display()); 37 | } 38 | 39 | parse_file(&file, args.dump, args.print_diagnostics)?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | fn parse_file(path: &Path, dump: bool, print_diagnostics: bool) -> anyhow::Result<()> { 46 | let contents = std::fs::read(path)?; 47 | let ast = Parser::parse(Lexer::new(&contents)); 48 | 49 | if dump { 50 | println!("{:#?}", ast); 51 | } 52 | 53 | if print_diagnostics && !ast.diagnostics.is_empty() { 54 | for diagnostic in &ast.diagnostics { 55 | println!( 56 | "{} on line {}, column {}", 57 | diagnostic.kind.get_message(), 58 | diagnostic.span.start_line(&contents), 59 | diagnostic.span.start_column(&contents) 60 | ); 61 | } 62 | } 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/cmd/tokenise.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use pxp_lexer::Lexer; 5 | use pxp_token::TokenKind; 6 | 7 | use crate::utils::find_php_files_in; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(version, about = "Tokenise a file or directory.")] 11 | pub struct Tokenise { 12 | #[arg(help = "The path to a file or directory.")] 13 | path: PathBuf, 14 | 15 | #[arg(short, long, help = "Dump the tokens to stdout.")] 16 | dump: bool, 17 | } 18 | 19 | pub fn tokenise(args: Tokenise) -> anyhow::Result<()> { 20 | let files = if args.path.is_dir() { 21 | find_php_files_in(&args.path)? 22 | } else { 23 | vec![args.path] 24 | }; 25 | 26 | for file in files { 27 | tokenise_file(&file, args.dump)?; 28 | } 29 | 30 | Ok(()) 31 | } 32 | 33 | fn tokenise_file(path: &PathBuf, dump: bool) -> anyhow::Result<()> { 34 | let contents = std::fs::read(path)?; 35 | let mut lexer = Lexer::new(&contents); 36 | 37 | loop { 38 | let current = lexer.current(); 39 | 40 | if dump { 41 | println!("{:?} - {:?}", current.kind, current.symbol); 42 | } 43 | 44 | if current.kind == TokenKind::Eof { 45 | break; 46 | } 47 | 48 | lexer.next(); 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{ 2 | builder::{styling::AnsiColor, Styles}, 3 | Parser, 4 | }; 5 | 6 | mod cmd; 7 | mod utils; 8 | 9 | const STYLES: Styles = Styles::styled() 10 | .header(AnsiColor::Green.on_default().bold()) 11 | .usage(AnsiColor::Green.on_default().bold()) 12 | .literal(AnsiColor::Blue.on_default().underline()) 13 | .placeholder(AnsiColor::Cyan.on_default()); 14 | 15 | #[derive(Parser, Debug)] 16 | #[command(version, about, long_about = None, styles = STYLES)] 17 | struct Args { 18 | #[clap(subcommand)] 19 | cmd: Command, 20 | } 21 | 22 | #[derive(Parser, Debug)] 23 | enum Command { 24 | #[clap(alias = "tokenize")] 25 | Tokenise(cmd::Tokenise), 26 | Parse(cmd::Parse), 27 | Init(cmd::Init), 28 | Index(cmd::Index), 29 | } 30 | 31 | fn main() -> anyhow::Result<()> { 32 | let parsed = Args::parse(); 33 | 34 | match parsed.cmd { 35 | Command::Tokenise(args) => cmd::tokenise(args), 36 | Command::Parse(args) => cmd::parse(args), 37 | Command::Init(args) => cmd::init(args), 38 | Command::Index(args) => cmd::index(args), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/stubs/.gitignore: -------------------------------------------------------------------------------- 1 | !pxp.config.toml 2 | -------------------------------------------------------------------------------- /src/stubs/pxp.config.toml: -------------------------------------------------------------------------------- 1 | [check] 2 | paths = [ 3 | "" 4 | ] 5 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use indicatif::{ProgressDrawTarget, ProgressStyle}; 4 | 5 | pub(crate) struct ProgressBar { 6 | bar: indicatif::ProgressBar, 7 | } 8 | 9 | impl ProgressBar { 10 | pub(crate) fn new(show: bool, n: u64) -> Self { 11 | let bar = indicatif::ProgressBar::new(n).with_style( 12 | ProgressStyle::with_template("{wide_bar:.green} {pos:>7}/{len:7}\n{msg}").unwrap(), 13 | ); 14 | 15 | if !show { 16 | bar.set_draw_target(ProgressDrawTarget::hidden()); 17 | } 18 | 19 | Self { bar } 20 | } 21 | 22 | pub(crate) fn inc(&self, n: u64) { 23 | self.bar.inc(n); 24 | } 25 | 26 | pub(crate) fn set_message(&self, message: String) { 27 | self.bar.set_message(message); 28 | } 29 | 30 | pub(crate) fn finish_and_clear(&self) { 31 | self.bar.finish_and_clear(); 32 | } 33 | } 34 | 35 | pub(crate) fn find_php_files_in(path: &Path) -> anyhow::Result> { 36 | let mut files = vec![]; 37 | 38 | for entry in path.read_dir()? { 39 | let entry = entry?; 40 | let path = entry.path(); 41 | 42 | if path.is_dir() { 43 | files.append(&mut find_php_files_in(&path)?); 44 | } else if path.extension().map_or(false, |ext| ext == "php") { 45 | files.push(path); 46 | } 47 | } 48 | 49 | Ok(files) 50 | } 51 | 52 | pub(crate) fn pxp_home_dir() -> anyhow::Result { 53 | let Some(home) = homedir::my_home()? else { 54 | anyhow::bail!("Could not find home directory."); 55 | }; 56 | 57 | let pxp = home.join(".pxp"); 58 | 59 | if !pxp.exists() { 60 | std::fs::create_dir(&pxp)?; 61 | } 62 | 63 | Ok(pxp) 64 | } 65 | --------------------------------------------------------------------------------