tree-sitter-phpdoc
Version:
PHPDoc grammar for tree-sitter
524 lines (469 loc) • 14.5 kB
JavaScript
const PHP = require('tree-sitter-php/grammar');
// PHPDoc reference: https://docs.phpdoc.org/3.0/guide/references/phpdoc/index.html
// TODO array return types https://docs.phpdoc.org/3.0/guide/references/phpdoc/types.html#arrays
// PHPDoc tags: https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/index.html#tag-reference
// PHPDoc docs appear to use these conventions
// TODO inline tags in inline tags (allowed for @internal)
// @verbatimElement [required element] [<optional element>]
module.exports = grammar({
name: 'phpdoc',
extras: $ => [
token(choice(
// Skip over stars at the beginnings of lines
seq(/\n/, /[ \t]*/, repeat(seq('*', /[ \t]*/))),
/\s/
))
],
conflicts: $ => [
[$.primitive_type, $.static],
[$.namespace_name],
[$.namespace_name_as_prefix],
],
// Note:
// 1. External scanners receive text with `extras` not removed yet. So
// whitespace must be detected and skipped appropriately with lexer->skip().
//
// 2. External scanners are triggered with highest precedence whenever possible.
// For example a rule like:
// seq(
// optional($.variable_name),
// optional($.text)
// )
// would not even start scanning for $.variable but immediately call the
// external scanner for $.text, because both tokens are optional. This can't
// be fixed with prec() either.
//
// That's why you see weird looking external tokens like $._text_after_type
// which allows us to restate the above rule like:
//
// choice(
// $._text_after_type,
// seq($.variable, $text)
// )
externals: $ => [
$.text,
$._text_after_type,
$._text_in_inline_tag,
$._text_not_version,
],
word: $ => $.name,
rules: {
document: $ => seq(
$._begin,
optional($.description),
repeat($.tag),
$._end,
),
_begin: $ => token(seq('/**', repeat('*'))),
description: $ => repeat1(
choice(
$.text,
$.inline_tag
)
),
_description_after_type: $ => alias(
repeat1(
choice(
alias($._text_after_type, $.text),
$.inline_tag
)
),
$.description
),
_description_not_version: $ => alias(
repeat1(
choice(
alias($._text_not_version, $.text),
$.inline_tag
)
),
$.description
),
_description_in_inline_tag: $ => alias(
repeat1(
alias($._text_in_inline_tag, $.text),
),
$.description
),
_description_in_inline_tag_with_nesting: $ => alias(
repeat1(
choice(
alias($._text_in_inline_tag, $.text),
$.inline_tag
)
),
$.description
),
tag: $ => choice(
$._tag_without_description,
$._tag_with_optional_description,
$._tag_with_required_description,
$._author_tag,
$._deprecated_tag,
$._global_tag,
$._link_tag,
$._method_tag,
$._param_tag,
$._property_tag,
$._return_tag,
$._see_tag,
$._since_tag,
$._throws_tag,
$._var_tag,
$._version_tag,
$._phpunit_tag,
// TODO eliminate this
$._tag_with_incomplete_implementation
),
inline_tag: $ => seq(
'{',
choice(
alias('@inheritdoc', $.tag_name),
alias('@inheritDoc', $.tag_name),
$._inline_internal_tag,
$._inline_link_tag,
$._inline_see_tag,
),
'}'
),
// @api
// @filesource
// @inheritdoc
// @inheritDoc
_tag_without_description: $ => alias(
choice(
'@api',
'@filesource',
'@inheritdoc',
'@inheritDoc',
),
$.tag_name
),
// @ignore [<description>]
// @internal [<description>]
_tag_with_optional_description: $ => seq(
alias(
choice(
'@ignore',
'@internal'
),
$.tag_name
),
optional($.description)
),
// @category [description]
// @copyright [description]
// @todo [description]
_tag_with_required_description: $ => seq(
alias(
choice(
'@category',
'@copyright',
'@todo',
),
$.tag_name
),
$.description
),
// TODO complete implementation for these tags
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/subpackage.html
// @subpackage [name]
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/example.html
// @example [location] [<start-line> [<number-of-lines>] ] [<description>]
// (also inline)
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/license.html
// @license [<url>] [name]
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/package.html
// @package [level 1]\\[level 2]\\[etc.]
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/source.html
// @source [<start-line> [<number-of-lines>] ] [<description>]
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/uses.html
// @uses [FQSEN] [<description>]
_tag_with_incomplete_implementation: $ => seq(
alias(
choice(
'@example',
'@license',
'@package',
'@source',
'@subpackage',
'@uses'
),
$.tag_name
),
optional($.description)
),
// @author [name] [<email address>]
// specs require email address to be wrapped in angle brackets
_author_tag: $ => seq(
alias('@author', $.tag_name),
$.author_name,
optional(seq('<', $.email_address, '>'))
),
// @global [Type] [name] (name w/o $)
// @global [Type] [description]
_global_tag: $ => seq(
alias('@global', $.tag_name),
$._type,
$.variable_name
),
// @internal [description]
_inline_internal_tag: $ => seq(
alias('@internal', $.tag_name),
optional($._description_in_inline_tag_with_nesting)
),
// @link [URI] [<description>]
_link_tag: $ => seq(
alias('@link', $.tag_name),
$.uri,
optional($.description)
),
_inline_link_tag: $ => seq(
alias('@link', $.tag_name),
$.uri,
optional($._description_in_inline_tag)
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/method.html#method
// @method [[static] return type] [name]([[type] [parameter]<, ...>]) [<description>]
_method_tag: $ => seq(
alias('@method', $.tag_name),
// `[static]` is interpreted as optional despite not being in [<...>]
optional($.static),
$._type,
$.name,
$.parameters,
optional($.description),
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/param.html
// @param [<Type>] [name] [<description>]
_param_tag: $ => seq(
alias('@param', $.tag_name),
optional($._type),
$.variable_name,
optional($.description),
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/property.html
// @property[<-read|-write>] [Type] [name] [<description>]
_property_tag: $ => seq(
alias(choice(
'@property',
'@property-read',
'@property-write',
), $.tag_name),
$._type,
$.variable_name,
optional($.description),
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/return.html
// @return [Type] [<description>]
_return_tag: $ => seq(
alias('@return', $.tag_name),
$._type,
optional($.description),
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/see.html
// @see [URI | FQSEN] [<description>]
// TODO Implement FQSEN support
// Docs on FQSEN are unclear:
// https://docs.phpdoc.org/classes/phpDocumentor-Descriptor-DescriptorAbstract.html#property_fqsen
// "Fully Qualified Structural Element Name; the FQCN including method, property or constant name"
// So FQSEN must always use FQCN (fully qualified class name). But the examples don't:
// @see number_of()
// @see MyClass::$items
// @see MyClass::setItems()
_see_tag: $ => seq(
alias('@see', $.tag_name),
choice(
$.uri,
alias($.qualified_name, $.fqsen),
alias(seq($.qualified_name, '()'), $.fqsen),
),
optional($.description)
),
_inline_see_tag: $ => seq(
alias('@see', $.tag_name),
choice(
$.uri,
alias($.qualified_name, $.fqsen),
alias(seq($.qualified_name, '()'), $.fqsen),
),
optional($._description_in_inline_tag)
),
// @throws [Type] [<description>]
_throws_tag: $ => seq(
alias('@throws', $.tag_name),
$._type,
optional($.description),
),
// https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/var.html
// @var ["Type"] [element_name] [<description>]
_var_tag: $ => seq(
alias('@var', $.tag_name),
choice(
// @var int|string[]|array<string, int> description
seq($._type, $._description_after_type),
// @var int|string[]|array<string, int>
seq($._type, $.variable_name),
// @var int|string[]|array<string, int> $foo description
seq($._type, $.variable_name, $.description),
)
),
// @deprecated [<Semantic Version>] [<description>]
_deprecated_tag: $ => seq(
alias('@deprecated', $.tag_name),
optional(
choice(
$.version,
$._description_not_version,
seq($.version, $.description)
)
)
),
// @since [<Semantic Version>] [<description>]
_since_tag: $ => seq(
alias('@since', $.tag_name),
choice(
$.version,
$._description_not_version,
seq($.version, $.description)
)
),
// @version [<Semantic Version>] [<description>]
_version_tag: $ => seq(
alias('@version', $.tag_name),
choice(
$.version,
$._description_not_version,
seq($.version, $.description)
)
),
// partial support for phpunit tags
// TODO id the "core" tags and flesh out their support (ie some tags take
// no text, some take types, etc)
// curl https://phpunit.readthedocs.io/en/9.5/annotations.html | grep '@' | sed 's/@/@/g' | sed -E 's/^.+@/@/' | sed -E 's/<.+$//' | sort | uniq
_phpunit_tag: $ => seq(
alias(
choice(
'@after',
'@afterClass',
'@annotation',
// '@author', // already part of phpdoc
'@backupGlobals',
'@backupStaticAttributes',
'@before',
'@beforeClass',
'@codeCoverageIgnore',
'@codeCoverageIgnore*',
'@codeCoverageIgnoreEnd',
'@codeCoverageIgnoreStart',
'@covers',
'@coversDefaultClass',
'@coversDefaultClass to shorten annotations',
'@coversNothing',
'@dataProvider',
'@depends',
'@depends annotation to express dependencies',
'@doesNotPerformAssertions',
'@group',
'@large',
'@medium',
'@preserveGlobalState',
'@requires',
'@requires usages',
'@runInSeparateProcess',
'@runTestsInSeparateProcesses',
'@small',
'@test',
'@testWith',
'@testdox',
'@ticket',
// '@uses', // already part of phpdoc
),
$.tag_name
),
optional($.description),
),
// PHP.rules._type creates an alias for $.type_list
_type: $ => PHP.rules._type,
// union_type uses _types, so we override it to be "regular" types (which
// aren't grouped under a parent node) or array types (which are)
_types: $ => choice(
$._regular_types,
alias($._phpdoc_array_types, $.array_type),
alias($._psalm_generic_array_types, $.array_type),
alias($._psalm_list_array_types, $.array_type)
),
_regular_types: $ => PHP.rules._types,
_phpdoc_array_types: $ => seq(
$._regular_types,
repeat1("[]")
),
_psalm_generic_array_types: $ => seq(
field('array', $._regular_types),
"<",
field('key', $._regular_types),
",",
field('value', $._regular_types),
">"
),
_psalm_list_array_types: $ => seq(
field('array', choice(alias("list", $.primitive_type), $._regular_types)),
"<",
field('value', $._regular_types),
">"
),
name: $ => PHP.rules.name,
named_type: $ => PHP.rules.named_type,
namespace_name: $ => PHP.rules.namespace_name,
namespace_name_as_prefix: $ => PHP.rules.namespace_name_as_prefix,
optional_type: $ => PHP.rules.optional_type,
primitive_type: $ => PHP.rules.primitive_type,
qualified_name: $ => PHP.rules.qualified_name,
union_type: $ => PHP.rules.union_type,
variable_name: $ => PHP.rules.variable_name,
// Match as many words as possible, where a word is just a sequence of
// non-whitespace and non-< characters, separated by a space. (The non-<
// requirement makes sure this regex doesn't consume the <email> field.)
author_name: $ => /\S+( [^\s<]+)*/,
// Simplisitic regex to match anything@anything.anything, where the last
// anything also doesn't include a closing angle bracket
email_address: $ => /\S+@\S+\.[^\s>]+/,
version: $ => choice(
// phpDoc does only recommend semantic versioning but it's not mandatory.
// So we gracefully accept every word (i.e. no space included) that
// starts with a number + dot as version.
/\d+\.[^\s]+/,
// Version vectors:
// $Id$
// name-of-vcs: $Id$
$._version_vector,
seq(/[a-zA-Z-_]+: */, $._version_vector),
// Used by PEAR
'@package_version@'
),
_version_vector: $ => /\$[a-zA-Z_][a-zA-Z0-9-_]*\$/,
uri: $ => /\w+:(\/?\/?)[^\s}]+/,
parameters: $ => seq(
'(',
sep($.parameter, ','),
')',
),
parameter: $ => seq(
optional($._type),
$.variable_name,
optional(seq(
'=',
$.default_value,
)),
),
default_value: $ => /[^, ][^,)]*/,
static: $ => 'static',
_end: $ => '*/',
},
})
function sep1(rule, sep) {
return seq(rule, repeat(seq(sep, rule)));
}
function sep(rule, sep) {
return optional(sep1(rule, sep));
}