import json import os import re from os import path from collections import OrderedDict from functools import cmp_to_key def find_in_parent(filename, starting_dir=None): '''Look up the directory tree until you find a file.''' root = find_root(filename, starting_dir) return path.join(root, filename) def find_root(filename, starting_dir=None): '''Look up the directory tree until you find a file.''' if starting_dir is None: starting_dir = os.getcwd() candidate_file = path.join(starting_dir, filename) if (path.isfile(candidate_file) or path.isdir(candidate_file)): return starting_dir elif starting_dir == '/': raise IOError('File not found in any directory') else: return find_root(filename, path.dirname(starting_dir)) def _parse_composer_manifest(path): '''Parse composer.json into dict''' with open(path, 'r') as composer_file: manifest = json.load(composer_file) return manifest def _get_package_name(manifest): '''Return the name field from composer.json.''' try: return manifest['name'] except KeyError: return 'foo/bar' def _get_all_autoloaders(manifest): '''Combine all PSR-0 and PSR-4 autoloaders''' autoloaders = dict() for type in ['psr-0', 'psr-4']: autoloaders.update(_get_autoloaders_by_type(manifest, type)) return dict((p, ns) for (ns, p) in autoloaders.items()) def _get_autoloaders_by_type(manifest, type): '''Get one kind of autoloader''' try: return manifest['autoload'][type] except KeyError: return dict() def match_path(autoloaders, dirname): '''Find namespace for a given path''' prefixes = sorted(autoloaders.keys(), key=len) try: prefix = next(p for p in prefixes if dirname.startswith(p)) namespace = autoloaders[prefix] remainder = dirname.replace(prefix, '', 1) except StopIteration: namespace = '' remainder = dirname return (namespace, remainder) def convert_path(dirname): '''Naively convert path to namespace''' return dirname.replace('/', '\\') def get_namespace(dirname): '''Convert dirname into namespace intelligently.''' try: composer_json = find_in_parent('composer.json', dirname) except IOError: return None composer_manifest = _parse_composer_manifest(composer_json) repo_root = path.dirname(composer_json) relative_path = path.relpath(dirname, repo_root) autoloaders = _get_all_autoloaders(composer_manifest) (namespace, remainder) = match_path(autoloaders, relative_path) return namespace + convert_path(remainder) def get_package_name(dirname): '''Find package name from current location.''' try: composer_json = find_in_parent('composer.json', dirname) except IOError: return 'foo/bar' composer_manifest = _parse_composer_manifest(composer_json) name = _get_package_name(composer_manifest) return name def prepare_arguments(param_tags): arguments = generate_arguments(param_tags) def add_argument(snip): '''Search for the next argument list, and insert one. These arguments will be of the form TypeName $variableName and will come from the body the @param snippet: * @param Typename $variableName Description ''' buffer = snip.buffer param_line_number = snip.snippet_start[0] param_line = buffer[param_line_number] try: argument_line_number = find_argument_line_number(param_line_number, buffer) except IndexError: return argument_line = buffer[argument_line_number] argument = get_argument_line(param_line) new_argument_line = insert_argument(argument, argument_line) buffer[argument_line_number] = new_argument_line def insert_argument(arg, line): insert_point = line.find(')') # No arguments yet, so no comma if line[insert_point - 1] is '(': return line[:insert_point] + arg + line[insert_point:] else: return line[:insert_point] + ', ' + arg + line[insert_point:] def find_argument_line_number(start, buffer): for line_number, line in enumerate(buffer[start:]): if line.endswith('*/'): return line_number + 1 + start else: pass raise IndexError('Could not find end of comment and start of method') def format_method(snip): '''Convert expanded snippet into method name and args.''' params = get_params_map(snip) if len(params) is 0: return sorted_params = sort_params(params.values()) delete_param_tags(snip, params) place_to_insert_params = snip.snippet_start[0] + list(params.keys())[0] snip.buffer[place_to_insert_params:place_to_insert_params] = sorted_params arguments = generate_arguments(sorted_params) arguments[-1] = arguments[-1].replace(',', '') args_line = snip.snippet_end[0] - 3 snip.buffer[args_line:args_line + 1] = arguments def delete_param_tags(snip, params): '''Sort @param PHPDoc tags so optional ones come later.''' for idx, relative_line_number in enumerate(params.keys()): # Have to subtract idx, because we are removing a line each time. line_number = relative_line_number + snip.snippet_start[0] - idx snip.buffer[line_number:line_number + 1] = [] def get_params_map(snip): '''Get a map of @param PHPDoc tags, line_number: line.''' lines = get_snippet_lines(snip) return OrderedDict([[k, v] for k, v in enumerate(lines) if '@param' in v]) def generate_arguments(params): '''Generate the function arguments from PHPDoc @params.''' return [get_argument_line(x) for x in params] def sort_params(params): '''Sort params to put optional arguments at the end.''' def sorter(left, right): [left_is_optional, right_is_optional] = \ [is_union_with_null(x) for x in [left, right]] if (left_is_optional and not right_is_optional): return 1 if (not left_is_optional and right_is_optional): return -1 return 0 return sorted(params, key=cmp_to_key(sorter)) def get_snippet_lines(snip): '''Get the relevant slice of a snippet buffer.''' start_line = snip.snippet_start[0] finish_line = snip.snippet_end[0] return snip.buffer[start_line:finish_line] def get_argument_line(param): '''Convert line from @param tag to method parameter.''' expression = '\s*\*\s+@param\s+([^\s]+)\s+(\$[^\s]+).*' result = re.search(expression, param) type = map_type(result.group(1)) name = result.group(2) return format_argument_line(type, name) def map_type(type): '''Convert PHPDoc type declaration into a PHP type hint.''' if (is_union_with_null(type)): return map_type(type.replace('|null', '')) if (is_union_type(type)): # PHP doesn't support union type hints yet. return None if (is_array_type(type)): # PHP doesn't have complex generics yet. return 'array' # A plain type return type def is_union_with_null(type): '''Test if PHPDoc type has a null option.''' return '|null' in type def is_union_type(type): '''Test if PHPDoc type is any kind of union.''' return '|' in type def is_array_type(type): '''Test if PHPDoc type is an array...''' return '[]' in type def format_argument_line(type, name): if (type is None): return name else: return '{} {}'.format(type, name)