flectra/doc/_extensions/autojsdoc/parser/jsdoc.py

331 lines
9.5 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
import re
import collections
import pyjsdoc
def strip_stars(doc_comment):
"""
Version of jsdoc.strip_stars which always removes 1 space after * if
one is available.
"""
return re.sub('\n\s*?\*[\t ]?', '\n', doc_comment[3:-2]).strip()
class ParamDoc(pyjsdoc.ParamDoc):
"""
Replace ParamDoc because FunctionDoc doesn't properly handle optional
params or default values (TODO: or compounds) if guessed_params is used
=> augment paramdoc with "required" and "default" items to clean up name
"""
def __init__(self, text):
super(ParamDoc, self).__init__(text)
# param name and doc can be separated by - or :, strip it
self.doc = self.doc.strip().lstrip('-:').lstrip()
self.optional = False
self.default = None
# there may not be a space between the param name and the :, in which
# case the : gets attached to the name, strip *again*
# TODO: formal @param/@property parser to handle this crap properly once and for all
self.name = self.name.strip().rstrip(':')
if self.name.startswith('['):
self.name = self.name.strip('[]')
self.optional = True
if '=' in self.name:
self.name, self.default = self.name.rsplit('=', 1)
def to_dict(self):
d = super(ParamDoc, self).to_dict()
d['optional'] = self.optional
d['default'] = self.default
return d
pyjsdoc.ParamDoc = ParamDoc
class CommentDoc(pyjsdoc.CommentDoc):
namekey = object()
is_constructor = False
@property
def name(self):
return self[self.namekey] or self['name'] or self['guessed_name']
def set_name(self, name):
# not great...
if name != '<exports>':
self.parsed['guessed_name'] = name
@property
def is_private(self):
return 'private' in self.parsed
def to_dict(self):
d = super(CommentDoc, self).to_dict()
d['name'] = self.name
return d
class PropertyDoc(CommentDoc):
@classmethod
def from_param(cls, s, sourcemodule=None):
parsed = ParamDoc(s).to_dict()
parsed['sourcemodule'] = sourcemodule
return cls(parsed)
@property
def type(self):
return self['type'].strip('{}')
def to_dict(self):
d = super(PropertyDoc, self).to_dict()
d['type'] = self.type
d['is_private'] = self.is_private
return d
class InstanceDoc(CommentDoc):
@property
def cls(self):
return self['cls']
def to_dict(self):
return dict(super(InstanceDoc, self).to_dict(), cls=self.cls)
class LiteralDoc(CommentDoc):
@property
def type(self):
if self['type']:
return self['type']
valtype = type(self['value'])
if valtype is bool:
return 'Boolean'
elif valtype is float:
return 'Number'
elif valtype is type(u''):
return 'String'
return ''
@property
def value(self):
return self['value']
def to_dict(self):
d = super(LiteralDoc, self).to_dict()
d['type'] = self.type
d['value'] = self.value
return d
class FunctionDoc(CommentDoc):
type = 'Function'
namekey = 'function'
@property
def is_constructor(self):
return self.name == 'init'
@property
def params(self):
tag_texts = self.get_as_list('param')
# turns out guessed_params is *almost* (?) always set to a list,
# if empty list of guessed params fall back to @params
if not self['guessed_params']:
# only get "primary" params (no "." in name)
return [
p for p in map(ParamDoc, tag_texts)
if '.' not in p.name
]
else:
param_dict = {}
for text in tag_texts:
param = ParamDoc(text)
param_dict[param.name] = param
return [param_dict.get(name) or ParamDoc('{} ' + name)
for name in self.get('guessed_params')]
@property
def return_val(self):
ret = self.get('return') or self.get('returns')
type = self.get('type')
if '{' in ret and '}' in ret:
if not '} ' in ret:
# Ensure that name is empty
ret = re.sub(r'\}\s*', '} ', ret)
return ParamDoc(ret)
if ret and type:
return ParamDoc('{%s} %s' % (type, ret))
return ParamDoc(ret)
def to_dict(self):
d = super(FunctionDoc, self).to_dict()
d['name'] = self.name
d['params'] = [param.to_dict() for param in self.params]
d['return_val']= self.return_val.to_dict()
return d
class NSDoc(CommentDoc):
namekey = 'namespace'
def __init__(self, parsed_comment):
super(NSDoc, self).__init__(parsed_comment)
self.members = collections.OrderedDict()
def add_member(self, name, member):
"""
:type name: str
:type member: CommentDoc
"""
member.set_name(name)
self.members[name] = member
@property
def properties(self):
if self.get('property'):
return [
(p.name, p)
for p in (
PropertyDoc.from_param(p, self['sourcemodule'])
for p in self.get_as_list('property')
)
]
return list(self.members.items()) or self['_members'] or []
def has_property(self, name):
return self.get_property(name) is not None
def get_property(self, name):
return next((p for n, p in self.properties if n == name), None)
def to_dict(self):
d = super(NSDoc, self).to_dict()
d['properties'] = [(n, p.to_dict()) for n, p in self.properties]
return d
class MixinDoc(NSDoc):
namekey = 'mixin'
class ModuleDoc(NSDoc):
namekey = 'module'
def __init__(self, parsed_comment):
super(ModuleDoc, self).__init__(parsed_comment)
#: callbacks to run with the modules mapping once every module is resolved
self._post_process = []
def post_process(self, modules):
for callback in self._post_process:
callback(modules)
@property
def module(self):
return self # lol
@property
def dependencies(self):
"""
Returns the immediate dependencies of a module (only those explicitly
declared/used).
"""
return self.get('dependency', None) or set()
@property
def exports(self):
"""
Returns the actual item exported from the AMD module, can be a
namespace, a class, a function, an instance, ...
"""
return self.get_property('<exports>')
def to_dict(self):
vars = super(ModuleDoc, self).to_dict()
vars['dependencies'] = self.dependencies
vars['exports'] = self.exports
return vars
class ClassDoc(NSDoc):
namekey = 'class'
@property
def constructor(self):
return self.get_property('init')
@property
def superclass(self):
return self['extends'] or self['base']
def get_property(self, method_name):
if method_name == 'extend':
return FunctionDoc({
'doc': 'Create subclass for %s' % self.name,
'guessed_function': 'extend',
})
# FIXME: should ideally be a proxy namespace
if method_name == 'prototype':
return self
return super(ClassDoc, self).get_property(method_name)
@property
def mixins(self):
return self.get_as_list('mixes')
def to_dict(self):
d = super(ClassDoc, self).to_dict()
d['mixins'] = self.mixins
return d
class UnknownNS(NSDoc):
def get_property(self, name):
return super(UnknownNS, self).get_property(name) or \
UnknownNS({'name': '{}.{}'.format(self.name, name)})
class Unknown(CommentDoc):
@classmethod
def from_(cls, source):
def builder(parsed):
inst = cls(parsed)
inst.parsed['source'] = source
return inst
return builder
@property
def name(self):
return self['name'] + ' ' + self['source']
@property
def type(self):
return "Unknown"
def get_property(self, p):
return Unknown(dict(self.parsed, source=self.name, name=p + '<'))
def parse_comments(comments, doctype=None):
# find last comment which starts with a *
docstring = next((
c['value']
for c in reversed(comments or [])
if c['value'].startswith(u'*')
), None) or u""
# \n prefix necessary otherwise parse_comment fails to take first
# block comment parser strips delimiters, but strip_stars fails without
# them
extract = '\n' + strip_stars('/*' + docstring + '\n*/')
parsed = pyjsdoc.parse_comment(extract, u'')
if doctype == 'FunctionExpression':
doctype = FunctionDoc
elif doctype == 'ObjectExpression' or doctype is None:
doctype = guess
if doctype is guess:
return doctype(parsed)
# in case a specific doctype is given, allow overriding it anyway
return guess(parsed, default=doctype)
def guess(parsed, default=NSDoc):
if 'class' in parsed:
return ClassDoc(parsed)
if 'function' in parsed:
return FunctionDoc(parsed)
if 'mixin' in parsed:
return MixinDoc(parsed)
if 'namespace' in parsed:
return NSDoc(parsed)
if 'module' in parsed:
return ModuleDoc(parsed)
if 'type' in parsed:
return PropertyDoc(parsed)
return default(parsed)