diff --git a/builtin_types_peg_rules.txt b/builtin_types_peg_rules.txt new file mode 100644 index 0000000..9ce1696 --- /dev/null +++ b/builtin_types_peg_rules.txt @@ -0,0 +1,124 @@ +BuiltinType + // primitives + = "number" &NoChar + / "string" &NoChar + / "boolean" &NoChar + / "undefined" &NoChar + / "null" &NoChar + + // standard types + / "Object" &NoChar + / "Function" &NoChar + / "Symbol" &NoChar + / "Error" &NoChar + / "EvalError" &NoChar + / "InternalError" &NoChar + / "RangeError" &NoChar + / "ReferenceError" &NoChar + / "SyntaxError" &NoChar + / "TypeError" &NoChar + / "URIError" &NoChar + / "Date" &NoChar + / "RegExp" &NoChar + / "Array" &NoChar + / "Int8Array" &NoChar + / "Uint8Array" &NoChar + / "Uint8ClampedArray" &NoChar + / "Int16Array" &NoChar + / "Uint16Array" &NoChar + / "Int32Array" &NoChar + / "Uint32Array" &NoChar + / "Float32Array" &NoChar + / "Float64Array" &NoChar + / "BigInt64Array" &NoChar + / "BigUint64Array" &NoChar + / "Promise" &NoChar + / "Generator" &NoChar + / "GeneratorFunction" &NoChar + / "AsyncFunction" &NoChar + / "XMLHttpRequest" &NoChar + / "ArrayBuffer" &NoChar + / "SharedArrayBuffer" &NoChar + / "Atomics" &NoChar + / "DataView" &NoChar + / "JSON" &NoChar + / "Map" &NoChar + / "Set" &NoChar + / "WeakMap" &NoChar + / "WeakSet" &NoChar + / "Reflect" &NoChar + / "Proxy" &NoChar + + // Geolocation + / "PositionError" &NoChar + / "PositionOptions" &NoChar + / "Position" &NoChar + / "Geolocation" &NoChar + + // DOM + / "Attr" &NoChar + / "CDATASection" &NoChar + / "CharacterData" &NoChar + / "ChildNode" &NoChar + / "Comment" &NoChar + / "CustomEvent" &NoChar + / "Document" &NoChar + / "DocumentFragment" &NoChar + / "DocumentType" &NoChar + / "DOMError" &NoChar + / "DOMException" &NoChar + / "DOMImplementation" &NoChar + / "DOMString" &NoChar + / "DOMTimeStamp" &NoChar + / "DOMStringList" &NoChar + / "DOMTokenList" &NoChar + / "Element" &NoChar + / "Event" &NoChar + / "EventTarget" &NoChar + / "HTMLCollection" &NoChar + / "MutationObserver" &NoChar + / "MutationRecord" &NoChar + / "NamedNodeMap" &NoChar + / "Node" &NoChar + / "NodeFilter" &NoChar + / "NodeIterator" &NoChar + / "NodeList" &NoChar + / "NonDocumentTypeChildNode" &NoChar + / "ParentNode" &NoChar + / "ProcessingInstruction" &NoChar + / "Selection" &NoChar + / "Range" &NoChar + / "Text" &NoChar + / "TextDecoder" &NoChar + / "TextEncoder" &NoChar + / "TimeRanges" &NoChar + / "TreeWalker" &NoChar + / "URL" &NoChar + / "Window" &NoChar + / "Worker" &NoChar + / "XMLDocument" &NoChar + + // HTML Types + / "HTMLElement" &NoChar + / "HTMLCanvasElement" &NoChar + / "HTMLImageElement" &NoChar + / "HTMLVideoElement" &NoChar + + // PointerEvent + / "PointerEvent" &NoChar + + // TouchEvent + / "TouchEvent" &NoChar + + // MouseEvent + / "MouseEvent" &NoChar + + // TypeScript types + / "*" &NoChar + // "?" will be catched by a special rule in type_rewrite_peg_rules + / "any" &NoChar + / "void" &NoChar + / "Partial" &NoChar + + // Other special types + / "Class" &NoChar diff --git a/index.js b/index.js index 5cded48..4471239 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const path = require('path'); const fs = require('fs'); const env = require('jsdoc/env'); const addInherited = require('jsdoc/augment').addInherited; +const peg = require("pegjs"); const config = env.conf.typescript; if (!config) { @@ -17,18 +18,57 @@ if (!fs.existsSync(moduleRootAbsolute)) { } const importRegEx = /import\(["']([^"']*)["']\)\.([^ \.\|\}><,\)=#\n]*)([ \.\|\}><,\)=#\n])/g; -const typedefRegEx = /@typedef \{[^\}]*\} (\S+)/; +const typedefRegEx = /@typedef \{[^\}]*\} (\S+)/g; const noClassdescRegEx = /@(typedef|module|type)/; const slashRegEx = /\\/g; const moduleInfos = {}; const fileNodes = {}; +let differences = 0; + +const pegRules = fs.readFileSync(path.join(__dirname, "./type_rewrite_peg_rules.txt"), 'utf8') + + '\n\n' + generateBuiltinTypeRules(); + +function makeRule(name, rules) { + return name + '\n = ' + rules.sort().reverse().join('\n / ') + '\n'; +} + +function generateBuiltinTypeRules() { + const types = []; + function readFile(name) { + const path = require.resolve(name, { paths: [__dirname, moduleRootAbsolute]}); + const content = fs.readFileSync(path, 'utf8'); + const typeMatches = content.matchAll(/^(interface|type)\s*(\w*)/gm); + for (const match of typeMatches) { + types.push(`"${match[2]}"`); + } + } + readFile('typescript/lib/lib.dom.d.ts'); + readFile('typescript/lib/lib.es5.d.ts'); + readFile('typescript/lib/lib.webworker.d.ts'); + return `BuiltinType\n = w:Word & { return [${types.sort().join(',')}].includes(flatten(w)) }` +} + +function buildTypeRewriteRules(identifiers, parser, currentSourceName) { + const rules = []; + for (const key of Object.keys(identifiers)) { + const identifier = identifiers[key]; + const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value); + const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, ''); + if (getModuleInfo(moduleId, parser)) { + const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : key; + const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser); + const replacement = `module:${moduleId.replace(slashRegEx, '/')}${exportName ? delimiter + exportName : ''}`; + rules.push(`"${key}" & NoChar { return "${replacement}" }`); + } else { + rules.push(`"${key}" & NoChar`); + } + } + return pegRules + '\n\n' + makeRule('RewriteType', rules); +} function getModuleInfo(moduleId, parser) { if (!moduleInfos[moduleId]) { - const moduleInfo = moduleInfos[moduleId] = { - namedExports: {} - }; if (!fileNodes[moduleId]) { const absolutePath = path.join(process.cwd(), moduleRoot, moduleId + '.js'); if (!fs.existsSync(absolutePath)) { @@ -37,6 +77,9 @@ function getModuleInfo(moduleId, parser) { const file = fs.readFileSync(absolutePath, 'UTF-8'); fileNodes[moduleId] = parser.astBuilder.build(file, absolutePath); } + const moduleInfo = moduleInfos[moduleId] = { + namedExports: {} + }; const node = fileNodes[moduleId]; if (node.program && node.program.body) { const classDeclarations = {}; @@ -75,12 +118,17 @@ exports.astNodeVisitor = { const modulePath = path.relative(path.join(process.cwd(), moduleRoot), currentSourceName).replace(/\.js$/, ''); fileNodes[modulePath] = node; const identifiers = {}; + let templateParameters = []; if (node.program && node.program.body) { const nodes = node.program.body; for (let i = 0, ii = nodes.length; i < ii; ++i) { let node = nodes[i]; + let leadingComments = node.leadingComments; if (node.type === 'ExportNamedDeclaration' && node.declaration) { node = node.declaration; + if (node.leadingComments) { + leadingComments = node.leadingComments; + } } if (node.type === 'ImportDeclaration') { node.specifiers.forEach(specifier => { @@ -98,6 +146,21 @@ exports.astNodeVisitor = { default: } }); + } else if (node.type === 'VariableDeclaration') { + for (const declaration of node.declarations) { + let declarationComments = leadingComments; + if (declaration.leadingComments) { + declarationComments = declaration.leadingComments; + } + if (declarationComments && declarationComments.length > 0) { + const comment = declarationComments[declarationComments.length - 1].value; + if (/@enum/.test(comment)) { + identifiers[declaration.id.name] = { + value: path.basename(currentSourceName) + }; + } + } + } } else if (node.type === 'ClassDeclaration') { if (node.id && node.id.name) { identifiers[node.id.name] = { @@ -190,38 +253,71 @@ exports.astNodeVisitor = { } // Treat `@typedef`s like named exports - const typedefMatch = comment.value.replace(/\s*\*\s*/g, ' ').match(typedefRegEx); - if (typedefMatch) { - identifiers[typedefMatch[1]] = { + const typedefMatches = comment.value.replace(/\s*\*\s*/g, ' ').matchAll(typedefRegEx); + for (const match of typedefMatches) { + identifiers[match[1]] = { value: path.basename(currentSourceName) }; } + + // Gather template Parameters + const templateMatches = comment.value.replace(/\s*,\s*/g, ',').matchAll(/@template\s+(?:\{[^}]+}\s+)?([\w,]+)/g); + for (const match of templateMatches) { + templateParameters = templateParameters.concat(match[1].split(',')); + } }); - node.comments.forEach(comment => { + if (Object.keys(identifiers).length > 0) { // Replace local types with the full `module:` path - Object.keys(identifiers).forEach(key => { - const eventRegex = new RegExp(`@(event |fires )${key}(\\s*)`, 'g'); - replace(eventRegex); - const typeRegex = new RegExp(`@(.*[{<|,]\\s*[!?]?)${key}(=?\\s*[}>|,])`, 'g'); - replace(typeRegex); + let templateRule = ''; + if (templateParameters.length > 0) { + templateRule = makeRule('TemplateParameter', templateParameters.map(t => `"${t}" & NoChar`)); + } else { + templateRule = 'TemplateParameter = & { return false }\n'; + } - function replace(regex) { - if (regex.test(comment.value)) { - const identifier = identifiers[key]; - const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value); - const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, ''); - if (getModuleInfo(moduleId, parser)) { - const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : key; - const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser); - let replacement = `module:${moduleId.replace(slashRegEx, '/')}${exportName ? delimiter + exportName : ''}`; - comment.value = comment.value.replace(regex, '@$1' + replacement + '$2'); + const rules = buildTypeRewriteRules(identifiers, parser, currentSourceName) + + '\n' + templateRule; + + const rewriter = peg.generate(rules); + + node.comments.forEach(comment => { + const before = comment.value; + + comment.value = rewriter.parse(comment.value); + + let regexVersion = before; + + Object.keys(identifiers).forEach(key => { + const eventRegex = new RegExp(`@(event |fires )${key}([^A-Za-z])`, 'g'); + replace(eventRegex); + + const typeRegex = new RegExp(`@(.*[{<|,(!?:]\\s*)${key}([^A-Za-z].*?\}|\})`, 'g'); + replace(typeRegex); + + function replace(regex) { + if (regex.test(regexVersion)) { + const identifier = identifiers[key]; + const absolutePath = path.resolve(path.dirname(currentSourceName), identifier.value); + const moduleId = path.relative(path.join(process.cwd(), moduleRoot), absolutePath).replace(/\.js$/, ''); + if (getModuleInfo(moduleId, parser)) { + const exportName = identifier.defaultImport ? getDefaultExportName(moduleId, parser) : key; + const delimiter = identifier.defaultImport ? '~' : getDelimiter(moduleId, exportName, parser); + let replacement = `module:${moduleId.replace(slashRegEx, '/')}${exportName ? delimiter + exportName : ''}`; + regexVersion = regexVersion.replace(regex, '@$1' + replacement + '$2'); + } } } + }); + + if (comment.value !== regexVersion) { + console.log(before); + console.log(comment.value); + differences++; } }); - }); + } } } } @@ -232,5 +328,6 @@ exports.handlers = { parseComplete: function(e) { // Build inheritance chain after adding @extends annotations addInherited(e.doclets, e.doclets.index); + console.log(`Differences: ${differences}`); } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c9d6160 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "jsdoc-plugin-typescript", + "version": "2.0.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "pegjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=" + } + } +} diff --git a/package.json b/package.json index fd34b23..532dbb7 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,14 @@ ], "license": "BSD-2-Clause", "peerDependencies": { - "jsdoc": ">=3.6.0" + "jsdoc": ">=3.6.0", + "typescript": "3.*" }, "repository": { "type": "git", "url": "git://github.com/openlayers/jsdoc-plugin-typescript.git" + }, + "dependencies": { + "pegjs": "^0.10.0" } } diff --git a/type_rewrite_peg_rules.txt b/type_rewrite_peg_rules.txt new file mode 100644 index 0000000..5b9e589 --- /dev/null +++ b/type_rewrite_peg_rules.txt @@ -0,0 +1,107 @@ +{ + function flatten(input) { + return Array.isArray(input) ? input.map(i => flatten(i)).join("") : input; + } +} + +All + = all: Lines { return flatten(all); } + +Lines + = Line "\n" Lines + / Line + +Line + = _ "*" _ TypeExpecting _ ComplexType ("#" [^\n\r]*)? + / _ "*" _ CurlyTypeExpecting _ CurlyType [^\n\r]* + / _ "*" _ !TypeExpecting !CurlyTypeExpecting [^\n\r]* + / _ [^\n\r*] [^\n\r]* + / _ + +CurlyTypeExpecting + = "@param" + / "@property" + / "@return" "s"? + / "@typedef" + / "@type" + / "@this" + / "@enum" + +TypeExpecting + = "@fires" + / "@event" + +CurlyType + = "{" _ TypeList _ "}" + +TypeList + = ComplexType _b "|" _b TypeList + / ComplexType + +ComplexType + = [?!]? ("...")? ComplexTypeUnmodified "="? + / "?" // short curcuit for the builtin type "?" + +ComplexTypeUnmodified + = "(" _ TypeList _ ")" + / FunctionType + / SimpleType ("."? "<" ParameterList ">")? + / InlineObjectType + +InlineObjectType + = "{" _b InlineObjectPropertyList _b "}" + +InlineObjectPropertyList + = InlineObjectProperty _ "," _b InlineObjectPropertyList + / InlineObjectProperty + +InlineObjectProperty + = Word _ ":" _b ComplexTypeUnmodified + +FunctionType + = "function" _ "(" _ (("this:" / "new:")? _b ParameterList)? ")" (_ ":" _b ComplexType)? + +ParameterList + = TypeList "," _b ParameterList + / TypeList + +SimpleType + = RewriteType + / PrimitiveType + / SpecialType + / ConvertedType + / TemplateParameter + / BuiltinType + +ConvertedType + = "module:" [a-zA-Z/-]+ (("~" / ".") [a-zA-Z]+)? & NoChar + +PrimitiveType + = "number" & NoChar + / "boolean" & NoChar + / "string" & NoChar + / "Array" & NoChar + / "Object" & NoChar + / "undefined" & NoChar + / "null" & NoChar + +SpecialType + = "*" & NoChar + / "any" & NoChar + / "void" & NoChar + / "Partial" & NoChar + / "Class" & NoChar + // "?" will be captured by a special rule above + +_b "break or whitespace" + = _ "\n" _ "*" _ + / _ + +_ "whitespace" + = [ \t]* + +NoChar + = [^A-Za-z0-9_] + +Word + = [A-Za-z_][A-Za-z0-9_]* & NoChar \ No newline at end of file