diff --git a/jsdocs.py b/jsdocs.py index 3f81588..c01f131 100644 --- a/jsdocs.py +++ b/jsdocs.py @@ -69,7 +69,7 @@ def getParser(view): return JsdocsJava(viewSettings) elif sourceLang == 'rust': return JsdocsRust(viewSettings) - elif sourceLang == 'ts': + elif sourceLang == 'ts' or sourceLang == 'tsx': return JsdocsTypescript(viewSettings) return JsdocsJavascript(viewSettings) @@ -373,6 +373,11 @@ def parse(self, line): def formatVar(self, name, val, valType=None): out = [] + + #for basic test with no value or type + if not val and not valType: + return out + if not valType: if not val or val == '': # quick short circuit valType = "[type]" @@ -1536,7 +1541,7 @@ class JsdocsTypescript(JsdocsParser): def setupSettings(self): identifier = '[a-zA-Z_$][a-zA-Z_$0-9]*' base_type_identifier = r'%s(\.%s)*(\[\])?' % ((identifier, ) * 2) - parametric_type_identifier = r'%s(\s*<\s*%s(\s*,\s*%s\s*)*>)?' % ((base_type_identifier, ) * 3) + parametric_type_identifier = r'((\s*[|]?\s*%s(\s*<\s*%s(\s*,\s*%s\s*)*>)?)+)' % ((base_type_identifier, ) * 3) self.settings = { # curly brackets around the type information "curlyTypes": True, @@ -1545,35 +1550,78 @@ def setupSettings(self): # technically, they can contain all sorts of unicode, but w/e "varIdentifier": identifier, "fnIdentifier": identifier, - "fnOpener": 'function(?:\\s+' + identifier + ')?\\s*\\(', + "typeIdentifier": parametric_type_identifier, + + "fnOpener": '(?:' + + r'function[\s*]*(?:' + identifier + r')?\s*\(' + + '|' + + '(?:' + identifier + r'|\(.*\)\s*=>)' + + '|' + + '(?:' + identifier + r'\s*\(.*\)\s*\{)' + + ')', + "commentCloser": " */", "bool": "Boolean", - "function": "Function", - "functionRE": - # Modifiers - r'(?:public|private|static)?\s*' - # Method name - + r'(?P' + identifier + r')\s*' - # Params - + r'\((?P.*)\)\s*' - # Return value - + r'(:\s*(?P' + parametric_type_identifier + r'))?', - "varRE": - r'((public|private|static|var)\s+)?(?P' + identifier - + r')\s*(:\s*(?P' + parametric_type_identifier - + r'))?(\s*=\s*(?P.*?))?([;,]|$)' + "function": "Function" } - self.functionRE = re.compile(self.settings['functionRE']) - self.varRE = re.compile(self.settings['varRE']) def parseFunction(self, line): - line = line.strip() - res = self.functionRE.search(line) + normalFunctions = ( + # Normal functions... + # fnName = function, fnName : function + r'(?:(?P' + self.settings['varIdentifier'] + r')\s*[:=]\s*)?' + + 'function' + # function fnName, function* fnName + + r'(?P[\s*]+)?(?P' + self.settings['fnIdentifier'] + ')?' + # (arg1, arg2) + + r'\s*\(\s*(?P.*)\)' + # :type + + r'(:\s*(?P' + self.settings['typeIdentifier'] + r'))?' + ) + + arrowFunctions = ( + # foo = + r'(?:(?P' + self.settings['varIdentifier'] + r')\s*[:=]\s*)?' + # () => y, x => y, (x, y) => y, (x = 4) => y + + r'(?:(?P' + self.settings['varIdentifier'] + r')|\(\s*(?P.*)\))' + # :type + + r'(:\s*(?P' + self.settings['typeIdentifier'] + r'))?' + # arrow + + r'\s*=>' + ) + + initializerShorthand = ( + # ES6 method initializer shorthand + # var person: InterfaceA = { getName() { return this.name; } } + r'(?P' + self.settings['varIdentifier'] + + ')\s*\((?P.*)\)\s*\{' + ) + + res = re.search(normalFunctions, line + ) or re.search(arrowFunctions, line + ) or re.search(initializerShorthand, line + ) if not res: return None - group_dict = res.groupdict() - return (group_dict["name"], group_dict["args"], group_dict["retval"]) + + groups = { + 'name1': '', + 'name2': '', + 'generator': '', + 'args': '', + 'args2': '', + 'retval': '' + } + groups.update(res.groupdict()) + + # grab the name out of "name1 = function name2(foo)" preferring name1 + generatorSymbol = '*' if (groups['generator'] or '').find('*') > -1 else '' + name = generatorSymbol + (groups['name1'] or groups['name2'] or '') + args = groups['args'] or groups['args2'] or '' + retval = groups['retval'] or '' + + return (name, args, retval) def getArgType(self, arg): if ':' in arg: @@ -1585,13 +1633,44 @@ def getArgName(self, arg): arg = arg.split(':')[0] return arg.strip('[ \?]') + def getArgInfo(self, arg): + if (re.search('^\{.*\}$', arg)): + subItems = splitByCommas(arg[1:-1]) + prefix = 'options.' + else: + subItems = [arg] + prefix = '' + + out = [] + for subItem in subItems: + out.append((self.getArgType(subItem), prefix + self.getArgName(subItem))) + + return out + def parseVar(self, line): - res = self.varRE.search(line) + + # var foo = blah, + # foo: string; + # foo: Array; + # baz.foo = blah; + # var foo = blah, + # baz = { + # foo : blah + # } + regex = ( + r'(?P' + self.settings['varIdentifier'] + ')' + # :type + + r'(:\s*(?P' + self.settings['typeIdentifier'] + r'))?' + # = value + + r'(\s*[=]\s*(?P.*?))?(?:[;,]|$)' + ) + + res = re.search(regex, line) if not res: return None - val = res.group('val') - if val: val = val.strip() - return (res.group('name'), val, res.group('type')) + + return (res.group('name'), res.group('val'), res.group('valType')) + def getFunctionReturnType(self, name, retval): return retval if retval != 'void' else None diff --git a/test_runner.py b/test_runner.py index f64a07a..a3cb93a 100644 --- a/test_runner.py +++ b/test_runner.py @@ -69,6 +69,360 @@ def assertDocBlockrResult(self, expected): self.assertEquals(expected, self.get_view_content()) +class TestTypeScript(ViewTestCase): + def get_syntax_file(self): + return 'Packages/TypeScript/TypeScript.tmLanguage' + + def test_basic(self): + self.set_view_content("\n/**|\nbasic") + self.run_doc_blockr() + self.assertDocBlockrResult('\n/**\n * \n */\nbasic') + + def test_empty_doc_blocks_are_created(self): + self.set_view_content('/**') + self.run_doc_blockr() + self.assertDocBlockrResult([ + "/**", + " * |CURSOR|", + " */" + ]) + + def test_that_function_template_is_added(self): + self.set_view_content('/**|\nfunction foo () {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @return {[type]} [description]', + ' */', + 'function foo () {' + ]) + + def test_that_function_return_type_is_added(self): + self.set_view_content('/**|\nfunction foo (): boolean {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @return {boolean} [description]', + ' */', + 'function foo (): boolean {' + ]) + + def test_that_function_return_void_is_not_added(self): + self.set_view_content('/**|\nfunction foo (): void {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' */', + 'function foo (): void {' + ]) + + def test_that_function_return_type_array_is_added(self): + self.set_view_content('/**|\nfunction foo (): Array {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @return {Array} [description]', + ' */', + 'function foo (): Array {' + ]) + + def test_parameters_are_added_to_function_templates(self): + self.set_view_content('/**|\nfunction foo (bar, baz) {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {[type]} bar [description]', + ' * @param {[type]} baz [description]', + ' * @return {[type]} [description]', + ' */', + 'function foo (bar, baz) {' + ]) + + def test_parameter_types_are_added_to_function_templates(self): + self.set_view_content('/**|\nfunction foo (bar: number, baz: string): boolean {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {number} bar [description]', + ' * @param {string} baz [description]', + ' * @return {boolean} [description]', + ' */', + 'function foo (bar: number, baz: string): boolean {' + ]) + + def test_parameter_types_are_added_to_arrow_functions(self): + self.set_view_content('/**|\nvar foo = (bar: number, callback: string => void): boolean => {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {number} bar [description]', + ' * @param {string => void} callback [description]', + ' * @return {boolean} [description]', + ' */', + 'var foo = (bar: number, callback: string => void): boolean => {' + ]) + + + def test_parameters_are_added_to_function_template_with_description_disabled(self): + self.set_view_content('/**|\nfunction foo (bar, baz) {') + self.view.settings().set('jsdocs_function_description', False) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * @param |SELECTION_BEGIN|{[type]}|SELECTION_END| bar [description]', + ' * @param {[type]} baz [description]', + ' * @return {[type]} [description]', + ' */', + 'function foo (bar, baz) {' + ]) + + def test_parameters_are_added_to_function_template_with_description_disabled_and_spacers_between_sections(self): + self.set_view_content('/**|\nfunction foo (bar, baz) {') + self.view.settings().set('jsdocs_function_description', False) + self.view.settings().set('jsdocs_spacer_between_sections', True) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * @param |SELECTION_BEGIN|{[type]}|SELECTION_END| bar [description]', + ' * @param {[type]} baz [description]', + ' *', + ' * @return {[type]} [description]', + ' */', + 'function foo (bar, baz) {' + ]) + + def test_parameters_are_added_to_function_template_with_description_disabled_and_spacer_after_description_isset(self): + self.set_view_content('/**|\nfunction foo (bar, baz) {') + self.view.settings().set('jsdocs_function_description', False) + self.view.settings().set('jsdocs_spacer_between_sections', 'after_description') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * @param |SELECTION_BEGIN|{[type]}|SELECTION_END| bar [description]', + ' * @param {[type]} baz [description]', + ' * @return {[type]} [description]', + ' */', + 'function foo (bar, baz) {' + ]) + + def test_params_across_multiple_lines_should_be_identified(self): + self.set_view_content([ + '/**|', + 'function foo(bar: number,', + ' baz:number,', + ' quux: string', + ' ):ClassA {' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {number} bar [description]', + ' * @param {number} baz [description]', + ' * @param {string} quux [description]', + ' * @return {ClassA} [description]', + ' */', + 'function foo(bar: number,', + ' baz:number,', + ' quux: string', + ' ):ClassA {' + ]) + + def test_vars_initialised_to_number_get_placeholders(self): + self.set_view_content([ + '/**|', + 'var foo = 1;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {Number}', + ' */', + 'var foo = 1;' + ]) + + def test_vars_string_double_quotes(self): + self.set_view_content([ + '/**|', + 'var foo = "a";' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {String}', + ' */', + 'var foo = "a";' + ]) + + def test_vars_string_single_quotes(self): + self.set_view_content([ + '/**|', + 'var foo = \'a\';' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {String}', + ' */', + 'var foo = \'a\';' + ]) + + def test_vars_unknown_type(self): + self.set_view_content([ + '/**|', + 'var foo = bar;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {[type]}', + ' */', + 'var foo = bar;' + ]) + + def test_vars_set_type_number(self): + self.set_view_content([ + '/**|', + 'var foo: number;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {number}', + ' */', + 'var foo: number;' + ]) + + def test_vars_const(self): + self.set_view_content([ + '/**|', + 'const foo: String = "THIS IS A STRING";' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {String}', + ' */', + 'const foo: String = "THIS IS A STRING";' + ]) + + def test_vars_let(self): + self.set_view_content([ + '/**|', + 'let foo: Bar;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {Bar}', + ' */', + 'let foo: Bar;' + ]) + + def test_vars_set_type_no_space(self): + self.set_view_content([ + '/**|', + 'var foo:number;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {number}', + ' */', + 'var foo:number;' + ]) + + def test_vars_set_type_class(self): + self.set_view_content([ + '/**|', + 'var foo: ClassA;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {ClassA}', + ' */', + 'var foo: ClassA;' + ]) + + def test_var_with_union_types(self): + self.set_view_content([ + '/**|', + 'var foo: ClassA | ClassB;' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @type {ClassA | ClassB}', + ' */', + 'var foo: ClassA | ClassB;' + ]) + + def test_return_value_with_union_types(self): + self.set_view_content([ + '/**|', + 'function foo(): ClassA | null {' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @return {ClassA | null} [description]', + ' */', + 'function foo(): ClassA | null {' + ]) + + def test_param_values_with_union_types(self): + self.set_view_content([ + '/**|', + 'function foo (input: string[] | null | Array): void {' + ]) + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {string[] | null | Array} input [description]', + ' */', + 'function foo (input: string[] | null | Array): void {' + ]) + + +class TestTypeScriptReact(ViewTestCase): + def get_syntax_file(self): + return 'Packages/TypeScript/TypeScriptReact.tmLanguage' + + # .tsx added to the TypeScript sourceLang + def test_parameters_are_added_to_function_templates(self): + self.set_view_content('/**|\nfunction foo (bar: number, baz: string): boolean {') + self.run_doc_blockr() + self.assertDocBlockrResult([ + '/**', + ' * |SELECTION_BEGIN|[foo description]|SELECTION_END|', + ' * @param {number} bar [description]', + ' * @param {string} baz [description]', + ' * @return {boolean} [description]', + ' */', + 'function foo (bar: number, baz: string): boolean {' + ]) + + class TestJavaScript(ViewTestCase): def get_syntax_file(self): @@ -428,7 +782,8 @@ def run(self): test_loader = unittest.TestLoader() # TODO move all test cases into tests directory and make test loader auto load testcases from the folder - + suite.addTests(test_loader.loadTestsFromTestCase(TestTypeScript)) + suite.addTests(test_loader.loadTestsFromTestCase(TestTypeScriptReact)) suite.addTests(test_loader.loadTestsFromTestCase(TestJavaScript)) suite.addTests(test_loader.loadTestsFromTestCase(TestPHP))