UNPKG

typescript-closure-tools

Version:

Command-line tools to convert closure-style JSDoc annotations to typescript, and to convert typescript sources to closure externs files

513 lines (512 loc) 18.6 kB
"use strict"; /// <reference path="../index/doctrine.d.ts"/> /// <reference path="../index/esprima.d.ts"/> /// <reference path="../index/escodegen.d.ts"/> Object.defineProperty(exports, "__esModule", { value: true }); const finder = require("./finder"); const escodegen = require("escodegen"); var reserved = [ 'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'class', 'enum', 'export', 'extends', 'import', 'super' ]; function last(values) { return values[values.length - 1]; } function find(values, predicate) { for (var i = 0; i < values.length; i++) { if (predicate(values[i])) return values[i]; } return null; } function get_in(object, keys) { for (var i = 0; i < keys.length; i++) if (object instanceof Object) object = object[keys[i]]; return object; } function generate_type_name(name) { if (name === 'Array') return 'any[]'; else return name; } function generate_dictionary_type(applications) { var tKey = generate_type(applications[0]); var tValue = generate_type(applications[1]); // Typescript only allows string and number in the key position switch (tKey) { case 'string': case 'number': break; default: tKey = 'string'; } return '{ [key: ' + tKey + ']: ' + tValue + ' }'; } function generate_type_application(expression, applications) { if (expression.name === 'Array') return generate_type(applications[0]) + '[]'; else if (expression.name === 'Object') return generate_dictionary_type(applications); else return generate_applied_type(expression) + '<' + applications.map(generate_type).join(',') + '>'; } function generate_indexed_function_parameter(type, index) { return '_' + index + ': ' + generate_type(type); } function generate_function_type(params, result) { var paramString = '(' + params.map(generate_indexed_function_parameter).join(', ') + ')'; return '{ ' + paramString + ': ' + generate_type(result) + ' }'; } function comment(text) { text = text.replace(/\/\*/g, '('); text = text.replace(/\*\//g, ')'); return '/*' + text + '*/'; } /** * Generate a type, without worrying about whether generics have been applied * @param t */ function generate_applied_type(t) { switch (t.type) { case 'NameExpression': references[t.name] = true; return generate_type_name(t.name); case 'TypeApplication': return generate_type_application(t.expression, t.applications); case 'OptionalType': case 'NullableType': case 'NonNullableType': return generate_type(t.expression); case 'FunctionType': return generate_function_type(t.params, t.result); case 'RestType': return generate_type(t.expression) + '[]'; case 'UnionType': return t.elements.map(generate_type).join('|'); case 'AllLiteral': case 'NullableLiteral': return 'any'; case 'NullLiteral': return 'any /*null*/'; case 'UndefinedLiteral': return 'any /*undefined*/'; case 'VoidLiteral': return 'void'; case 'RecordType': return '{ ' + t.fields.map(generate_record_field).join('; ') + ' }'; case 'ArrayType': return generate_type(t.elements[0]) + '[]'; case 'FieldType': case 'ParameterType': default: throw new Error('Unknown type expression ' + t.type); } } function generate_type(t) { if (!t) return 'any /*missing*/'; var result = generate_applied_type(t); // If t is a name expression, look for a definition and verify templates if (t.type === 'NameExpression') { var symbol = finder.symbol(t.name); var tags = get_in(symbol, ['', 'jsdoc', 'tags']) || []; var params = []; tags.filter(t => t.title === 'template') .forEach(tag => { tag.description.split(',').forEach(i => params.push('any')); }); if (params.length > 0) return t.name + '<' + params.join(', ') + '>'; } return result; } function safe_name(name) { if (reserved.indexOf(name) !== -1) return '_' + name; else return name; } function generate_function_parameter_name(annotation) { if (!annotation.type) return safe_name(annotation.name); else if (annotation.type.type === 'OptionalType') return safe_name(annotation.name) + '?'; else if (annotation.type.type === 'RestType') return '...' + safe_name(annotation.name); else return safe_name(annotation.name); } function generate_function_parameter(annotation) { return generate_function_parameter_name(annotation) + ': ' + generate_type(annotation.type); } function param_names(docs) { if (docs.value && docs.value.params) return docs.value.params.map(x => x.name); else return docs.jsdoc.tags.filter(t => t.title === 'param').map(x => x.name); } function tags_by_name(tags) { var acc = {}; tags.forEach(function (tag) { acc[tag.name] = tag; }); return acc; } function generate_var(name, docs) { var typeTag = find(docs.tags, t => t.title === 'type'); return 'var ' + name + ': ' + generate_type(typeTag && typeTag.type) + ';'; } var VOID_TYPE = { type: { type: 'NameExpression', name: 'void' } }; function generics(docs) { var tags = docs.tags || []; var templateTags = tags.filter(t => t.title === 'template'); var names = templateTags.map(x => x.description); if (names.length === 0) return ''; else return '<' + names.join(', ') + '>'; } // TODO now that overloads are no longer needed, this should return string not string[] function generate_functions(name, value) { var docs = value.jsdoc; var names = param_names(value); var tags = docs.tags || []; var paramTags = tags.filter(t => t.title === 'param'); var overloads = [tags_by_name(paramTags)]; return overloads.map(tagsByName => { var paramStrings = generate_param_strings(names, tagsByName); var returnTag = find(tags, t => t.title === 'return' || t.title === 'returns') || VOID_TYPE; return 'function ' + name + generics(docs) + '(' + paramStrings.join(', ') + '): ' + generate_type(returnTag.type) + ';'; }); } // TODO now that overloads are no longer needed, this should return string not string[] function generate_properties(name, value) { // Function if (is_function(value)) return generate_functions(name, value); // Field else return [generate_var(name, value.jsdoc)]; } function generate_param_strings(names, types) { return names.map(function (name) { if (name in types) return generate_function_parameter(types[name]); else if (name.substring(0, 4) === 'opt_') return safe_name(name) + '?: any /* jsdoc error */'; else return safe_name(name) + ': any /* jsdoc error */'; }); } // TODO now that overloads are no longer needed, this should return string not string[] function generate_methods(name, value) { var docs = value.jsdoc; var names = param_names(value); var tags = docs.tags || []; var paramTags = tags.filter(t => t.title === 'param'); var overloads = [tags_by_name(paramTags)]; return overloads.map(tagsByName => { var paramStrings = generate_param_strings(names, tagsByName); var returnTag = find(tags, t => t.title === 'return' || t.title === 'returns') || VOID_TYPE; return name + generics(docs) + '(' + paramStrings.join(', ') + '): ' + generate_type(returnTag.type); }); } function generate_field_name(name, type) { if (type && type.type === 'OptionalType') return name + '?'; else return name; } function generate_field(name, docs) { var tags = docs.tags || []; var typeTag = find(tags, t => t.title === 'type'); var type = typeTag && typeTag.type; var fieldName = generate_field_name(name, type); return fieldName + ': ' + generate_type(type); } function generate_record_field(field) { var fieldName = generate_field_name(field.key, field.value); return fieldName + ': ' + generate_type(field.value); } function is_function(value) { var tags = value.jsdoc.tags || []; return (value.value && value.value.type === 'FunctionExpression') || tags.some(t => t.title === 'param') || tags.some(t => t.title === 'return'); } // TODO now that overloads are no longer needed, this should return string not string[] function generate_members(name, value) { // Function if (is_function(value)) return generate_methods(name, value); // Field else return [generate_field(name, value.jsdoc)]; } function with_underscore(type) { var prefix = /[\w\.]+/.exec(type)[0]; var suffix = type.substring(prefix.length); return prefix + '__Class' + suffix; } function generate_class_extends(docs) { var tags = docs.tags || []; var supers = tags .filter(t => t.title === 'extends') .map(x => x.type) .map(generate_type) .map(with_underscore); if (supers.length > 0) return 'extends ' + supers.join(', ') + ' '; else return ''; } function generate_interface_extends(docs) { var tags = docs.tags || []; var supers = tags .filter(t => t.title === 'extends') .map(x => x.type) .map(generate_type); if (supers.length > 0) return 'extends ' + supers.join(', ') + ' '; else return ''; } function generate_implements(docs) { var supers = docs.tags .filter(t => t.title === 'implements') .map(x => x.type) .map(generate_type); if (supers.length > 0) return 'implements ' + supers.join(', ') + ' '; else return ''; } function generate_interface(name, prototype) { var constructor = prototype['']; var acc = 'interface ' + name + generics(constructor.jsdoc) + ' ' + generate_interface_extends(constructor.jsdoc) + '{\n'; Object.keys(prototype).filter(name => name !== '').forEach(function (name) { var value = prototype[name]; var text = value.originalText.replace(/\n/g, '\n '); acc += '\n'; generate_members(name, value).forEach(member => { acc += ' ' + text + '\n'; acc += ' ' + member + ';\n'; }); }); acc += '}'; return acc; } // TODO now that overloads are no longer needed, this should return string not string[] function generate_constructors(value) { var docs = value.jsdoc; var names = param_names(value); var paramTags = docs.tags.filter(t => t.title === 'param'); var overloads = [tags_by_name(paramTags)]; return overloads.map(tagsByName => { return 'constructor(' + generate_param_strings(names, tagsByName).join(', ') + ')'; }); } function get_type_name(tag) { if (tag.name) return tag.name; else if (tag.type) { if (tag.type.type === 'NameExpression') return tag.type.name; else if (tag.type.type === 'TypeApplication') return tag.type.expression.name; } } function generate_class(name, prototype) { var constructor = prototype['']; var acc = 'class ' + name + generics(constructor.jsdoc) + ' extends ' + name + '__Class' + generics(constructor.jsdoc) + ' { }\n'; acc += '/** Fake class which should be extended to avoid inheriting static properties */\n'; acc += 'class ' + name + '__Class' + generics(constructor.jsdoc) + ' ' + generate_class_extends(constructor.jsdoc) + generate_implements(constructor.jsdoc) + ' { \n'; var text = constructor.originalText.replace(/\n/g, '\n' + ' ' + ' '); acc += '\n'; generate_constructors(constructor).forEach(constructor => { acc += ' ' + ' ' + text + '\n'; acc += ' ' + ' ' + constructor + ';\n'; }); function add_members(prototype) { Object.keys(prototype).filter(name => name !== '').forEach(function (name) { var value = prototype[name]; var docs = value.jsdoc; var tags = docs.tags || []; var text = value.originalText.replace(/\n/g, '\n' + ' ' + ' '); if (!tags.some(t => t.title === 'override')) { acc += '\n'; generate_members(name, value).forEach(member => { acc += ' ' + ' ' + text + '\n'; acc += ' ' + ' ' + member + ';\n'; }); } }); // Look for implemented interfaces and inject them var constructor = prototype['']; var tags = constructor.jsdoc.tags || []; var names = tags .filter(t => t.title === 'implements') .map(get_type_name); var locations = names .map(finder.symbol) .filter(s => Boolean(s)); locations.forEach(add_members); } add_members(prototype); acc += '} \n'; return acc; } function generate_enum(name, values) { function key_id(property) { if ('name' in property) return property.name; else if ('value' in property) return property.value; else throw new Error('Unknown enum property ' + property); } function safe_id(name) { if (/^\w+$/.test(name)) return name; else return "'" + name + "'"; } if (values.type === 'ObjectExpression') { var keys = values.properties .map(x => x.key) .map(key_id) .map(safe_id); return 'enum ' + name + ' { ' + keys.join(', ') + ' } '; } // Otherwise try to follow references else { var reference = escodegen.generate(values); var dot = reference.lastIndexOf('.'); var moduleName = reference.substring(0, dot); var enumName = reference.substring(dot + 1); var fileName = finder.file(moduleName); if (!fileName) return 'enum ' + name + ' { /* ' + reference + ' */ } '; var symbols = finder.symbols(fileName); var moduleValue = symbols.modules[moduleName]; var enumValue = moduleValue[enumName].value; return generate_enum(name, enumValue); } } function generate_typedef(name, docs) { var tag = find(docs.tags, t => t.title === 'typedef'); var typedef = tag.type; switch (typedef.type) { // Skip modifiers case 'OptionalType': case 'NullableType': case 'NonNullableType': tag.type = typedef.expression; return generate_typedef(name, docs); // F function(...) becomes interface F { (...): ... } case 'FunctionType': var argumentString = typedef.params.map(generate_indexed_function_parameter).join(', '); var returnString = generate_type(typedef.result); return 'interface ' + name + ' {\n (' + argumentString + '): ' + returnString + '\n}'; // S { ... } becomes interface T { ... } case 'RecordType': var fieldsString = typedef.fields.map(function (field) { return field.key + ': ' + generate_type(field.value); }).join(';\n '); return 'interface ' + name + ' {\n ' + fieldsString + '\n}'; // T NamedType becomes interface T extends NamedType { } case 'NameExpression': references[typedef.name] = true; return 'type ' + name + ' = ' + typedef.name; // T NamedType<Param> becomes interface T extends NamedType<Param> { } case 'TypeApplication': if (typedef.expression.name === 'Object') return generate_dictionary_type(typedef.applications); else { var base = typedef.expression.name; var generics = '<' + typedef.applications.map(generate_type).join(',') + '>'; return 'interface ' + name + ' extends ' + base + generics + ' { }'; } case 'UnionType': var union = generate_type(typedef); return 'type ' + name + ' = ' + union + ';'; // Anything else becomes interface Name { /* explanation */ } default: return 'interface ' + name + ' { ' + comment(generate_type(typedef)) + ' }'; } } var references = {}; function defs(symbols) { var modules = {}; references = {}; // Generate classes Object.keys(symbols.classes).forEach(name => { var dot = name.lastIndexOf('.'); var moduleName = name.substring(0, dot); var propertyName = name.substring(dot + 1); var symbol = symbols.classes[name]; var constructor = symbol['']; if (!modules[moduleName]) modules[moduleName] = {}; if (constructor.jsdoc.tags.some(t => t.title === 'interface')) modules[moduleName][propertyName] = generate_interface(propertyName, symbol); else modules[moduleName][propertyName] = generate_class(propertyName, symbol); }); // Generate statics Object.keys(symbols.modules).forEach(moduleName => { var symbol = symbols.modules[moduleName]; if (!modules[moduleName]) modules[moduleName] = {}; Object.keys(symbol).forEach(propertyName => { var value = symbol[propertyName]; var comment = value.originalText + '\n'; if (value.jsdoc.tags.some(t => t.title === 'enum')) modules[moduleName][propertyName] = comment + generate_enum(propertyName, value.value); else if (value.jsdoc.tags.some(t => t.title === 'typedef')) modules[moduleName][propertyName] = comment + generate_typedef(propertyName, value.jsdoc); else { modules[moduleName][propertyName] = generate_properties(propertyName, value) .map(property => comment + property) .join('\n'); } }); }); return { modules: modules, references: Object.keys(references) }; } exports.defs = defs;