From 4a6e71e4815fad7e09113d97658ae4132c478ebc Mon Sep 17 00:00:00 2001 From: runem Date: Thu, 9 Jul 2020 16:17:46 +0200 Subject: [PATCH 1/2] Only run GH actions once per commit in pull requests --- .github/workflows/workflow.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 3265e9e3..0bc8de40 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,6 +1,10 @@ name: Main Workflow -on: [push, pull_request] +on: + pull_request: + push: + branches: + - master jobs: run: From 844035384df11150360b1abd3cc00a2dfeb1ed10 Mon Sep 17 00:00:00 2001 From: runem Date: Sat, 7 Nov 2020 12:48:56 +0100 Subject: [PATCH 2/2] Update experimental JSON output to be up-to-date with schema --- src/analyze/stages/discover-declarations.ts | 23 +- src/transformers/json2/json2-transformer.ts | 516 +++++++++++++------- src/transformers/json2/schema.ts | 380 +++++++++----- 3 files changed, 608 insertions(+), 311 deletions(-) diff --git a/src/analyze/stages/discover-declarations.ts b/src/analyze/stages/discover-declarations.ts index 7fac6620..a77d8f6b 100644 --- a/src/analyze/stages/discover-declarations.ts +++ b/src/analyze/stages/discover-declarations.ts @@ -1,7 +1,6 @@ import { SourceFile } from "typescript"; import { AnalyzerVisitContext } from "../analyzer-visit-context"; import { ComponentDeclaration } from "../types/component-declaration"; -import { resolveSymbolDeclarations } from "../util/ast-util"; import { analyzeComponentDeclaration } from "./analyze-declaration"; /** @@ -12,23 +11,11 @@ import { analyzeComponentDeclaration } from "./analyze-declaration"; export function discoverDeclarations(sourceFile: SourceFile, context: AnalyzerVisitContext): ComponentDeclaration[] { const declarations: ComponentDeclaration[] = []; - const symbol = context.checker.getSymbolAtLocation(sourceFile); - if (symbol != null) { - // Get all exports in the source file - const exports = context.checker.getExportsOfModule(symbol); - - // Find all class declarations in the source file - for (const symbol of exports) { - const node = symbol.valueDeclaration; - - if (node != null) { - if (context.ts.isClassDeclaration(node) /* || context.ts.isInterfaceDeclaration(node)*/) { - const nodes = resolveSymbolDeclarations(symbol); - const decl = analyzeComponentDeclaration(nodes, context); - if (decl != null) { - declarations.push(decl); - } - } + for (const statement of sourceFile.statements) { + if (context.ts.isClassDeclaration(statement) /* || context.ts.isInterfaceDeclaration(node)*/) { + const decl = analyzeComponentDeclaration([statement], context); + if (decl != null) { + declarations.push(decl); } } } diff --git a/src/transformers/json2/json2-transformer.ts b/src/transformers/json2/json2-transformer.ts index 4c94c094..97d3b601 100644 --- a/src/transformers/json2/json2-transformer.ts +++ b/src/transformers/json2/json2-transformer.ts @@ -1,40 +1,41 @@ import { basename, relative } from "path"; -import { isSimpleType, toSimpleType } from "ts-simple-type"; +import { isSimpleType, SimpleType, toSimpleType } from "ts-simple-type"; import * as tsModule from "typescript"; -import { Node, Program, SourceFile, Type, TypeChecker } from "typescript"; +import { Declaration, FunctionDeclaration, Node, Program, SourceFile, Symbol, Type, TypeChecker, VariableDeclaration } from "typescript"; import { AnalyzerResult } from "../../analyze/types/analyzer-result"; import { ComponentDeclaration, ComponentHeritageClause } from "../../analyze/types/component-declaration"; import { ComponentFeatureBase } from "../../analyze/types/features/component-feature"; import { JsDoc } from "../../analyze/types/js-doc"; -import { findParent, getNodeName, resolveDeclarations } from "../../analyze/util/ast-util"; +import { findParent, getNodeName, hasFlag, isAliasSymbol, resolveDeclarations } from "../../analyze/util/ast-util"; import { getMixinHeritageClauses, getSuperclassHeritageClause, visitAllHeritageClauses } from "../../analyze/util/component-declaration-util"; import { getJsDoc } from "../../analyze/util/js-doc-util"; import { arrayDefined } from "../../util/array-util"; -import { getTypeHintFromMethod } from "../../util/get-type-hint-from-method"; import { getTypeHintFromType } from "../../util/get-type-hint-from-type"; import { filterVisibility } from "../../util/model-util"; import { TransformerConfig } from "../transformer-config"; import { TransformerFunction } from "../transformer-function"; import { - AttributeDoc, - ClassDoc, - ClassMember, - CSSPartDoc, - CSSPropertyDoc, - CustomElementDefinitionDoc, - CustomElementDoc, - EventDoc, - ExportDoc, - FieldDoc, - FunctionDoc, - MethodDoc, - MixinDoc, - ModuleDoc, - PackageDoc, - Parameter, - Reference, - SlotDoc, - VariableDoc + Attribute as AttributeSchema, + ClassDeclaration as ClassDeclarationSchema, + ClassField as ClassFieldSchema, + ClassMember as ClassMemberSchema, + ClassMethod as ClassMethodSchema, + CssCustomProperty as CssCustomPropertySchema, + CssPart as CssPartSchema, + CustomElement as CustomElementSchema, + Declaration as DeclarationSchema, + Event as EventSchema, + Export as ExportSchema, + FunctionDeclaration as FunctionDeclarationSchema, + MixinDeclaration as MixinDeclarationSchema, + Module as ModuleSchema, + Package as PackageSchema, + Parameter as ParameterSchema, + Reference as ReferenceSchema, + Slot as SlotSchema, + Type as TypeSchema, + TypeReference as TypeReferenceSchema, + VariableDeclaration as VariableDeclarationSchema } from "./schema"; interface TransformerContext { @@ -42,10 +43,11 @@ interface TransformerContext { checker: TypeChecker; program: Program; ts: typeof tsModule; + emitDeclaration(decl: Node): void; } /** - * Transforms results to json using the schema found in the PR at https://github.com/webcomponents/custom-elements-json/pull/9 + * Transforms results to json using the schema found here: https://github.com/webcomponents/custom-elements-manifest * @param results * @param program * @param config @@ -55,155 +57,226 @@ export const json2Transformer: TransformerFunction = (results: AnalyzerResult[], config, checker: program.getTypeChecker(), program, - ts: tsModule + ts: tsModule, + emitDeclaration() { + // noop + } }; // Flatten analyzer results expanding inherited declarations into the declaration array. const flattenedAnalyzerResults = flattenAnalyzerResults(results); // Transform all analyzer results into modules - const modules = flattenedAnalyzerResults.map(result => analyzerResultToModuleDoc(result, context)); + const modules = flattenedAnalyzerResults.map(result => analyzerResultToModuleSchema(result, context)); - const htmlData: PackageDoc = { - version: "experimental", + const packageData: PackageSchema = { + schemaVersion: "0.0.1", // TODO: Find out what version this is modules }; - return JSON.stringify(htmlData, null, 2); + return JSON.stringify(packageData, null, 2); }; /** - * Transforms an analyzer result into a module doc + * Transforms an analyzer result into a Module * @param result * @param context */ -function analyzerResultToModuleDoc(result: AnalyzerResult, context: TransformerContext): ModuleDoc { - // Get all export docs from the analyzer result - const exports = getExportsDocsFromAnalyzerResult(result, context); +function analyzerResultToModuleSchema(result: AnalyzerResult, context: TransformerContext): ModuleSchema { + // Only "javascript-module" are analyzed for now + + const exportedDeclarations = new Set(); + + // Get all exports from the analyzer result + const exports = analyzerResultToExportSchema(result, { + ...context, + emitDeclaration(decl: Node) { + exportedDeclarations.add(decl); + } + }); + + // Get all declarations from the analyzer result + const declarations = analyzerResultToDeclarationSchema(exportedDeclarations, result, context); return { + kind: "javascript-module", path: getRelativePath(result.sourceFile.fileName, context), - exports: exports.length === 0 ? undefined : exports + exports: exports.length === 0 ? undefined : exports, + declarations, + summary: undefined, // TODO: include this field if the SourceFile has a top-level comment with JSDoc tag "@summary" + description: undefined // TODO: include this field if the SourceFile has a top-level comment without any JSDoc tags., }; } /** - * Returns ExportDocs in an analyzer result + * Returns Export from an analyzer result * @param result * @param context */ -function getExportsDocsFromAnalyzerResult(result: AnalyzerResult, context: TransformerContext): ExportDoc[] { - // Return all class- and variable-docs - return [ - ...getDefinitionDocsFromAnalyzerResult(result, context), - ...getClassDocsFromAnalyzerResult(result, context), - ...getVariableDocsFromAnalyzerResult(result, context), - ...getFunctionDocsFromAnalyzerResult(result, context) - ]; -} +function analyzerResultToExportSchema(result: AnalyzerResult, context: TransformerContext): ExportSchema[] { + const exports: ExportSchema[] = []; -/** - * Returns FunctionDocs in an analyzer result - * @param result - * @param context - */ -function getFunctionDocsFromAnalyzerResult(result: AnalyzerResult, context: TransformerContext): FunctionDoc[] { - // TODO: support function exports - return []; -} + // Add all custom element definitions to the Exports array + for (const componentDefinition of result.componentDefinitions) { + const declaration = componentDefinition.declaration?.node; -function getDefinitionDocsFromAnalyzerResult(result: AnalyzerResult, context: TransformerContext): CustomElementDefinitionDoc[] { - return arrayDefined( - result.componentDefinitions.map(definition => { - // It's not possible right now to model a tag name where the - // declaration couldn't be resolved because the "declaration" is required - if (definition.declaration == null) { - return undefined; - } - - return { - kind: "definition", - name: definition.tagName, - declaration: getReferenceForNode(definition.declaration.node, context) - }; - }) - ); -} + if (declaration == null) { + continue; + } -/** - * Returns VariableDocs in an analyzer result - * @param result - * @param context - */ -function getVariableDocsFromAnalyzerResult(result: AnalyzerResult, context: TransformerContext): VariableDoc[] { - const varDocs: VariableDoc[] = []; + exports.push({ + kind: "custom-element-definition", + name: componentDefinition.tagName, + declaration: nodeToReferenceSchema(declaration, context) + }); - // Get all export symbols in the source file - const symbol = context.checker.getSymbolAtLocation(result.sourceFile)!; - if (symbol == null) { - return []; + context.emitDeclaration(declaration); } - const exports = context.checker.getExportsOfModule(symbol); - - // Convert all export variables to VariableDocs - for (const exp of exports) { - switch (exp.flags) { - case tsModule.SymbolFlags.BlockScopedVariable: - case tsModule.SymbolFlags.Variable: { - const node = exp.valueDeclaration; - - if (tsModule.isVariableDeclaration(node)) { - // Get the nearest variable statement in order to read the jsdoc - const variableStatement = findParent(node, tsModule.isVariableStatement) || node; - const jsDoc = getJsDoc(variableStatement, tsModule); - - varDocs.push({ - kind: "variable", - name: node.name.getText(), - description: jsDoc?.description, - type: getTypeHintFromType(context.checker.getTypeAtLocation(node), context.checker, context.config), - summary: getSummaryFromJsDoc(jsDoc) - }); + // Get the symbol representing the source file + const fileSymbol = context.checker.getSymbolAtLocation(result.sourceFile); + + // Loop through all exports in the source file + for (const [exportName, exportSymbol] of ((fileSymbol?.exports?.entries() as unknown) || []) as Iterable<[string, Symbol]>) { + // Find corresponding definition from the AnalyzerResult and skip if found (we just added all custom element definitions above) + const hasCustomElementDefinition = result.componentDefinitions.some(definition => definition.declaration?.symbol === exportSymbol); + if (hasCustomElementDefinition) { + continue; + } + + // Resolve the symbol to a declaration Node + const declaration = resolveSymbolDeclaration(exportSymbol, context); + + // Handle namespace exports + if (context.ts.isNamespaceExport(declaration)) { + const sourceFile = resolveSymbolDeclaration(exportSymbol, context, { resolveAlias: true }) as Node; + + const module = context.ts.isSourceFile(sourceFile) + ? getRelativePath(sourceFile.fileName, context) + : declaration.parent.moduleSpecifier?.getText(); + + exports.push({ + kind: "js", + name: exportName, + declaration: { + module, + name: "*" } - break; + }); + } + + // Add export declarations + else if (context.ts.isExportDeclaration(declaration)) { + const isExportStar = hasFlag(exportSymbol.flags, context.ts.SymbolFlags.ExportStar); + + if (isExportStar) { + exports.push({ + kind: "js", + name: "*", + declaration: { + module: declaration.moduleSpecifier?.getText(), + name: "*" + } + }); + } else { + exports.push({ + kind: "js", + name: exportName, + declaration: nodeToReferenceSchema(exportSymbol, context) + }); + + context.emitDeclaration(declaration); } } + + // Anything else, such as ClassDeclaration and FunctionDeclaration + else if ( + context.ts.isClassDeclaration(declaration) || + context.ts.isFunctionDeclaration(declaration) || + context.ts.isVariableDeclaration(declaration) + ) { + exports.push({ + kind: "js", + name: exportName, + declaration: nodeToReferenceSchema(exportSymbol, context) + }); + + context.emitDeclaration(declaration); + } } - return varDocs; + return exports; } /** - * Returns ClassDocs in an analyzer result + * Converts all exported TS declarations to Declarations. + * These are converted and added "deep" (through the "emitDeclaration" callback), + * so that all declarations reachable from exports are described here. + * @param exportedDeclarations * @param result * @param context */ -function getClassDocsFromAnalyzerResult(result: AnalyzerResult, context: TransformerContext): (ClassDoc | CustomElementDoc | MixinDoc)[] { - const classDocs: ClassDoc[] = []; - - // Convert all declarations to class docs - for (const decl of result.declarations || []) { - const doc = getExportsDocFromDeclaration(decl, result, context); - if (doc != null) { - classDocs.push(doc); +function analyzerResultToDeclarationSchema( + exportedDeclarations: Set, + result: AnalyzerResult, + context: TransformerContext +): DeclarationSchema[] { + const declarationNodes = new Set(exportedDeclarations); + + context = { + ...context, + emitDeclaration(decl: Node) { + declarationNodes.add(decl); + } + }; + + const declarations: DeclarationSchema[] = []; + + const emittedNodes = new Set(); + + // Convert declarations until "delcarationNodes" is empty + let declNode: Node | undefined; + while ((declNode = declarationNodes.values().next()?.value) != null) { + declarationNodes.delete(declNode); + + if (emittedNodes.has(declNode) || declNode.getSourceFile() !== result.sourceFile) { + continue; + } + + emittedNodes.add(declNode); + + const componentDeclaration = result.declarations?.find(componentDecl => componentDecl?.node === declNode); + + // Convert the node + let declarationSchema: DeclarationSchema | undefined; + + if (componentDeclaration != null) { + declarationSchema = componentDeclarationToDeclarationSchema(componentDeclaration, result, context); + } else if (context.ts.isFunctionDeclaration(declNode)) { + declarationSchema = functionDeclarationToFunctionDeclarationSchema(declNode, context); + } else if (context.ts.isVariableDeclaration(declNode)) { + declarationSchema = variableDeclarationToVariableDeclarationSchema(declNode, context); + } + + if (declarationSchema != null) { + declarations.push(declarationSchema); } } - return classDocs; + return declarations; } /** - * Converts a component declaration to ClassDoc, CustomElementDoc or MixinDoc + * Converts a component declaration to ClassDeclaration, CustomElementDeclaration or MixinDeclaration * @param declaration * @param result * @param context */ -function getExportsDocFromDeclaration( +function componentDeclarationToDeclarationSchema( declaration: ComponentDeclaration, result: AnalyzerResult, context: TransformerContext -): ClassDoc | CustomElementDoc | MixinDoc | undefined { +): DeclarationSchema | undefined { // Only include "mixin" and "class" in the output. Interfaces are not outputted.. if (declaration.kind === "interface") { return undefined; @@ -211,15 +284,15 @@ function getExportsDocFromDeclaration( // Get the superclass of this declaration const superclassHeritage = getSuperclassHeritageClause(declaration); - const superclassRef = superclassHeritage == null ? undefined : getReferenceFromHeritageClause(superclassHeritage, context); + const superclassRef = superclassHeritage == null ? undefined : heritageClauseToReferenceSchema(superclassHeritage, context); // Get all mixins const mixinHeritage = getMixinHeritageClauses(declaration); - const mixinRefs = arrayDefined(mixinHeritage.map(h => getReferenceFromHeritageClause(h, context))); + const mixinRefs = arrayDefined(mixinHeritage.map(h => heritageClauseToReferenceSchema(h, context))); - const members = getClassMemberDocsFromDeclaration(declaration, context); + const members = componentDeclarationToClassMemberSchema(declaration, context); - const classDoc: ClassDoc | MixinDoc = { + const classDoc: ClassDeclarationSchema = { kind: "class", superclass: superclassRef, mixins: mixinRefs.length > 0 ? mixinRefs : undefined, @@ -229,25 +302,35 @@ function getExportsDocFromDeclaration( summary: getSummaryFromJsDoc(declaration.jsDoc) }; + // TODO: Properly implement mixins + if (declaration.kind === "mixin") { + const mixinDoc: MixinDeclarationSchema = { + ...classDoc, + kind: "mixin" + }; + + return mixinDoc; + } + // Find the first corresponding custom element definition for this declaration const definition = result.componentDefinitions.find(def => def.declaration?.node === declaration.node); if (definition != null) { - const events = getEventDocsFromDeclaration(declaration, context); - const slots = getSlotDocsFromDeclaration(declaration, context); - const attributes = getAttributeDocsFromDeclaration(declaration, context); - const cssProperties = getCSSPropertyDocsFromDeclaration(declaration, context); - const cssParts = getCSSPartDocsFromDeclaration(declaration, context); + const events = componentDeclarationToEventSchema(declaration, context); + const slots = componentDeclarationToSlotSchema(declaration, context); + const attributes = componentDeclarationToAttributeSchema(declaration, context); + const cssProperties = componentDeclarationToCSSPropertySchema(declaration, context); + const cssParts = componentDeclarationToCSSPartSchema(declaration, context); // Return a custom element doc if a definition was found - const customElementDoc: CustomElementDoc = { + const customElementDoc: CustomElementSchema = { ...classDoc, tagName: definition.tagName, events: events.length > 0 ? events : undefined, slots: slots.length > 0 ? slots : undefined, attributes: attributes.length > 0 ? attributes : undefined, cssProperties: cssProperties.length > 0 ? cssProperties : undefined, - cssParts: cssParts.length > 0 ? cssParts : undefined + parts: cssParts.length > 0 ? cssParts : undefined }; return customElementDoc; @@ -257,11 +340,43 @@ function getExportsDocFromDeclaration( } /** - * Returns event docs for a declaration + * Returns FunctionDeclaration in an analyzer result + * @param node + * @param context + */ +function functionDeclarationToFunctionDeclarationSchema( + node: FunctionDeclaration, + context: TransformerContext +): FunctionDeclarationSchema | undefined { + // TODO: support function exports + return undefined; +} + +/** + * Returns VariableDeclaration from an analyzer result + * @param node + * @param context + */ +function variableDeclarationToVariableDeclarationSchema(node: VariableDeclaration, context: TransformerContext): VariableDeclarationSchema { + // Get the nearest variable statement in order to read the jsdoc + const variableStatement = findParent(node, tsModule.isVariableStatement) || node; + const jsDoc = getJsDoc(variableStatement, tsModule); + + return { + kind: "variable", + name: node.name.getText(), + description: jsDoc?.description, + type: typeToTypeSchema(context.checker.getTypeAtLocation(node), context), + summary: getSummaryFromJsDoc(jsDoc) + }; +} + +/** + * Returns Events for a declaration * @param declaration * @param context */ -function getEventDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): EventDoc[] { +function componentDeclarationToEventSchema(declaration: ComponentDeclaration, context: TransformerContext): EventSchema[] { return filterVisibility(context.config.visibility, declaration.events).map(event => { const type = event.type?.() || { kind: "ANY" }; const simpleType = isSimpleType(type) ? type : toSimpleType(type, context.checker); @@ -272,61 +387,61 @@ function getEventDocsFromDeclaration(declaration: ComponentDeclaration, context: return { description: event.jsDoc?.description, name: event.name, - inheritedFrom: getInheritedFromReference(declaration, event, context), - type: typeName == null || simpleType.kind === "ANY" ? "Event" : typeName, + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, event, context), + type: typeToTypeSchema(typeName == null || simpleType.kind === "ANY" ? "Event" : typeName, context) || { type: "Event" }, detailType: customEventDetailType != null ? getTypeHintFromType(customEventDetailType, context.checker, context.config) : undefined }; }); } /** - * Returns slot docs for a declaration + * Returns Slots for a declaration * @param declaration * @param context */ -function getSlotDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): SlotDoc[] { +function componentDeclarationToSlotSchema(declaration: ComponentDeclaration, context: TransformerContext): SlotSchema[] { return declaration.slots.map(slot => ({ description: slot.jsDoc?.description, name: slot.name || "", - inheritedFrom: getInheritedFromReference(declaration, slot, context) + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, slot, context) })); } /** - * Returns css properties for a declaration + * Returns CSSProperties for a declaration * @param declaration * @param context */ -function getCSSPropertyDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): CSSPropertyDoc[] { +function componentDeclarationToCSSPropertySchema(declaration: ComponentDeclaration, context: TransformerContext): CssCustomPropertySchema[] { return declaration.cssProperties.map(cssProperty => ({ name: cssProperty.name, description: cssProperty.jsDoc?.description, type: cssProperty.typeHint, default: cssProperty.default != null ? JSON.stringify(cssProperty.default) : undefined, - inheritedFrom: getInheritedFromReference(declaration, cssProperty, context) + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, cssProperty, context) })); } /** - * Returns css parts for a declaration + * Returns CSSParts for a declaration * @param declaration * @param context */ -function getCSSPartDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): CSSPartDoc[] { +function componentDeclarationToCSSPartSchema(declaration: ComponentDeclaration, context: TransformerContext): CssPartSchema[] { return declaration.cssParts.map(cssPart => ({ name: cssPart.name, description: cssPart.jsDoc?.description, - inheritedFrom: getInheritedFromReference(declaration, cssPart, context) + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, cssPart, context) })); } /** - * Returns attribute docs for a declaration + * Returns Attributes for a declaration * @param declaration * @param context */ -function getAttributeDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): AttributeDoc[] { - const attributeDocs: AttributeDoc[] = []; +function componentDeclarationToAttributeSchema(declaration: ComponentDeclaration, context: TransformerContext): AttributeSchema[] { + const attributeDocs: AttributeSchema[] = []; for (const member of filterVisibility(context.config.visibility, declaration.members)) { if (member.attrName != null) { @@ -335,8 +450,8 @@ function getAttributeDocsFromDeclaration(declaration: ComponentDeclaration, cont fieldName: member.propName, defaultValue: member.default != null ? JSON.stringify(member.default) : undefined, description: member.jsDoc?.description, - type: getTypeHintFromType(member.typeHint || member.type?.(), context.checker, context.config), - inheritedFrom: getInheritedFromReference(declaration, member, context) + type: typeToTypeSchema(member.typeHint || member.type?.(), context), + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, member, context) }); } } @@ -345,24 +460,24 @@ function getAttributeDocsFromDeclaration(declaration: ComponentDeclaration, cont } /** - * Returns class member docs for a declaration + * Returns ClassMember for a declaration * @param declaration * @param context */ -function getClassMemberDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): ClassMember[] { - return [...getFieldDocsFromDeclaration(declaration, context), ...getMethodDocsFromDeclaration(declaration, context)]; +function componentDeclarationToClassMemberSchema(declaration: ComponentDeclaration, context: TransformerContext): ClassMemberSchema[] { + return [...componentDeclarationToClassFieldSchema(declaration, context), ...componentDeclarationToClassMethodSchema(declaration, context)]; } /** - * Returns method docs for a declaration + * Returns ClassMethod for a declaration * @param declaration * @param context */ -function getMethodDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): MethodDoc[] { - const methodDocs: MethodDoc[] = []; +function componentDeclarationToClassMethodSchema(declaration: ComponentDeclaration, context: TransformerContext): ClassMethodSchema[] { + const methodDocs: ClassMethodSchema[] = []; for (const method of filterVisibility(context.config.visibility, declaration.methods)) { - const parameters: Parameter[] = []; + const parameters: ParameterSchema[] = []; let returnType: Type | undefined = undefined; const node = method.node; @@ -375,11 +490,7 @@ function getMethodDocsFromDeclaration(declaration: ComponentDeclaration, context parameters.push({ name: name, - type: getTypeHintFromType( - typeHint || (param.type != null ? context.checker.getTypeAtLocation(param.type) : undefined), - context.checker, - context.config - ), + type: typeToTypeSchema(typeHint || (param.type != null ? context.checker.getTypeAtLocation(param.type) : undefined), context), description: description }); } @@ -398,14 +509,13 @@ function getMethodDocsFromDeclaration(declaration: ComponentDeclaration, context kind: "method", name: method.name, privacy: method.visibility, - type: getTypeHintFromMethod(method, context.checker), description: method.jsDoc?.description, parameters, return: { description: returnDescription, - type: getTypeHintFromType(returnTypeHint || returnType, context.checker, context.config) + type: typeToTypeSchema(returnTypeHint || returnType, context) }, - inheritedFrom: getInheritedFromReference(declaration, method, context), + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, method, context), summary: getSummaryFromJsDoc(method.jsDoc) // TODO: "static" }); @@ -415,12 +525,12 @@ function getMethodDocsFromDeclaration(declaration: ComponentDeclaration, context } /** - * Returns field docs from a declaration + * Returns FieldDocs from a declaration * @param declaration * @param context */ -function getFieldDocsFromDeclaration(declaration: ComponentDeclaration, context: TransformerContext): FieldDoc[] { - const fieldDocs: FieldDoc[] = []; +function componentDeclarationToClassFieldSchema(declaration: ComponentDeclaration, context: TransformerContext): ClassFieldSchema[] { + const fieldDocs: ClassFieldSchema[] = []; for (const member of filterVisibility(context.config.visibility, declaration.members)) { if (member.propName != null) { @@ -429,9 +539,9 @@ function getFieldDocsFromDeclaration(declaration: ComponentDeclaration, context: name: member.propName, privacy: member.visibility, description: member.jsDoc?.description, - type: getTypeHintFromType(member.typeHint || member.type?.(), context.checker, context.config), + type: typeToTypeSchema(member.typeHint || member.type?.(), context), default: member.default != null ? JSON.stringify(member.default) : undefined, - inheritedFrom: getInheritedFromReference(declaration, member, context), + inheritedFrom: componentDeclarationToInheritedReferenceSchema(declaration, member, context), summary: getSummaryFromJsDoc(member.jsDoc) // TODO: "static" }); @@ -441,13 +551,13 @@ function getFieldDocsFromDeclaration(declaration: ComponentDeclaration, context: return fieldDocs; } -function getInheritedFromReference( +function componentDeclarationToInheritedReferenceSchema( onDeclaration: ComponentDeclaration, feature: ComponentFeatureBase, context: TransformerContext -): Reference | undefined { +): ReferenceSchema | undefined { if (feature.declaration != null && feature.declaration !== onDeclaration) { - return getReferenceForNode(feature.declaration.node, context); + return nodeToReferenceSchema(feature.declaration.node, context); } return undefined; @@ -458,7 +568,11 @@ function getInheritedFromReference( * @param node * @param context */ -function getReferenceForNode(node: Node, context: TransformerContext): Reference { +function nodeToReferenceSchema(node: Node | Symbol, context: TransformerContext): ReferenceSchema { + if (!("getSourceFile" in node)) { + node = resolveSymbolDeclaration(node, context); + } + const sourceFile = node.getSourceFile(); const name = getNodeName(node, context) as string; @@ -472,6 +586,8 @@ function getReferenceForNode(node: Node, context: TransformerContext): Reference }; } + context.emitDeclaration(node); + // Test if the source file is located in a package const packageName = getPackageName(sourceFile); if (packageName != null) { @@ -556,19 +672,19 @@ function getReturnFromJsDoc(jsDoc: JsDoc | undefined): { description?: string; t * @param heritage * @param context */ -function getReferenceFromHeritageClause(heritage: ComponentHeritageClause, context: TransformerContext): Reference | { name: string } | undefined { +function heritageClauseToReferenceSchema(heritage: ComponentHeritageClause, context: TransformerContext): ReferenceSchema | undefined { const node = heritage.declaration?.node; const identifier = heritage.identifier; // Return a reference for this node if any if (node != null) { - return getReferenceForNode(node, context); + return nodeToReferenceSchema(node, context); } // Try to get declaration of the identifier if no node was found const [declaration] = resolveDeclarations(identifier, context); if (declaration != null) { - return getReferenceForNode(declaration, context); + return nodeToReferenceSchema(declaration, context); } // Just return the name of the reference if nothing could be resolved @@ -642,3 +758,57 @@ function getSummaryFromJsDoc(jsDoc: JsDoc | undefined): string | undefined { return summaryTag.comment; } + +function resolveSymbolDeclaration(symbol: Symbol, context: TransformerContext, { resolveAlias }: { resolveAlias?: boolean } = {}): Declaration { + if (resolveAlias && isAliasSymbol(symbol, context.ts)) { + symbol = context.checker.getAliasedSymbol(symbol); + } + + const decl = symbol.valueDeclaration || symbol.getDeclarations()?.[0]; + if (decl == null) { + throw new Error(`Couldn't find declaration for symbol: ${symbol.name}`); + } + + return decl; +} + +/** + * Converts a Typescript Type to a TypeSchema + * @param type + * @param context + */ +function typeToTypeSchema(type: string | Type | SimpleType | undefined, context: TransformerContext): TypeSchema | undefined { + if (type == null) { + return undefined; + } + + const typeReferences: TypeReferenceSchema[] = []; + + if (typeof type !== "string" && "flags" in type && type.symbol != null) { + // Emit the reference + const declNode = resolveSymbolDeclaration(type.symbol, context); + + if (declNode != null) { + const ref = nodeToReferenceSchema(declNode, context); + + typeReferences.push({ + ...ref, + start: declNode.getStart(), + end: declNode.getEnd() + }); + + context.emitDeclaration(declNode); + } + } + + const typeHint = getTypeHintFromType(type, context.checker, context.config); + + if (typeHint == null) { + return undefined; + } + + return { + type: typeHint, + references: typeReferences.length > 0 ? typeReferences : undefined + }; +} diff --git a/src/transformers/json2/schema.ts b/src/transformers/json2/schema.ts index 81a49bfa..33399a9f 100644 --- a/src/transformers/json2/schema.ts +++ b/src/transformers/json2/schema.ts @@ -1,26 +1,54 @@ /** - * This file comes from the following PR with a proposed JSON schema: - * https://github.com/webcomponents/custom-elements-json/pull/9 + * @license + * Copyright (c) 2019 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ /** - * The top-level interface of a custom-elements.json file. + * The top-level interface of a custom elements manifest file. * - * custom-elements.json documents all the elements in a single npm package, - * across all modules within the package. Elements may be exported from multiple - * modules with re-exports, but as a rule, elements in this file should be - * included once in the "canonical" module that they're exported from. + * Because custom elements are JavaScript classes, describing a custom element + * may require describing arbitrary JavaScript concepts like modules, classes, + * functions, etc. So custom elements manifests are capable of documenting + * the elements in a package, as well as those JavaScript concepts. + * + * The modules described in a package should be the public entrypoints that + * other packages may import from. Multiple modules may export the same object + * via re-exports, but in most cases a package should document the single + * canonical export that should be used. */ -export interface PackageDoc { - version: string; +export interface Package { + /** + * The version of the schema used in this file. + */ + schemaVersion: string; + + /** + * The Markdown to use for the main readme of this package. + * + * This can be used to override the readme used by Github or npm if that + * file contains information irrelevant to custom element catalogs and + * documentation viewers. + */ + readme?: string; /** * An array of the modules this package contains. */ - modules: Array; + modules: Array; } -export interface ModuleDoc { +// This type may expand in the future to include JSON, CSS, or HTML +// modules. +export type Module = JavaScriptModule; + +export interface JavaScriptModule { + kind: "javascript-module"; + path: string; /** @@ -33,16 +61,81 @@ export interface ModuleDoc { */ description?: string; - exports?: Array; + /** + * The declarations of a module. + * + * For documentation purposes, all declarations that are reachable from + * exports should be described here. Ie, functions and objects that may be + * properties of exported objects, or passed as arguments to functions. + */ + declarations: Array; + + /** + * The exports of a module. This includes JavaScript exports and + * custom element definitions. + */ + exports?: Array; +} + +export type Export = JavaScriptExport | CustomElementExport; + +export interface JavaScriptExport { + kind: "js"; + + /** + * The name of the exported symbol. + * + * JavaScript has a number of ways to export objects which determine the + * correct name to use. + * + * - Default exports must use the name "default". + * - Named exports use the name that is exported. If the export is renamed + * with the "as" clause, use the exported name. + * - Aggregating exports (`* from`) should use the name `*` + */ + name: string; + + /** + * A reference to the exported declaration. + * + * In the case of aggregating exports, the reference's `module` field must be + * defined and the `name` field must be `"*"`. + */ + declaration: Reference; +} + +/** + * A global custom element defintion, ie the result of a + * `customElements.define()` call. + * + * This is represented as an export because a definition makes the element + * available outside of the module it's defined it. + */ +export interface CustomElementExport { + kind: "custom-element-definition"; + + /** + * The tag name of the custom element. + */ + name: string; + + /** + * A reference to the class or other declaration that implements the + * custom element. + */ + declaration: Reference; } -export type ExportDoc = ClassDoc | FunctionDoc | VariableDoc | CustomElementDefinitionDoc; +export type Declaration = ClassDeclaration | FunctionDeclaration | MixinDeclaration | VariableDeclaration | CustomElement; /** * A reference to an export of a module. * * All references are required to be publically accessible, so the canonical - * representation of a refernce it the export it's available from. + * representation of a reference is the export it's available from. + * + * Referrences to global symbols like `Array`, `HTMLElement`, or `Event` + * */ export interface Reference { name: string; @@ -50,48 +143,77 @@ export interface Reference { module?: string; } -export interface CustomElementDoc extends ClassDoc { - tagName: string; +/** + * Description of a custom element class. + * + * Custom elements are JavaScript classes, so this extends from + * `ClassDeclaration` and adds custom-element-specific features like + * attributes, events, and slots. + * + * Note that `tagName` in this interface is optional. Tag names are not + * neccessarily part of a custom element class, but belong to the definition + * (often called the "registration") or the `customElements.define()` call. + * + * Because classes and tag anmes can only be registered once, there's a + * one-to-one relationship between classes and tag names. For ease of use, + * we allow the tag name here. + * + * Some packages define and register custom elements in separate modules. In + * these cases one `Module` should contain the `CustomElement` without a + * tagName, and another `Module` should contain the + * `CustomElement`. + */ +export interface CustomElement extends ClassDeclaration { + /** + * An optional tag name that should be specified if this is a + * self-registering element. + * + * Self-registering elements must also include a CustomElementExport + * in the module's exports. + */ + tagName?: string; + /** * The attributes that this element is known to understand. */ - attributes?: AttributeDoc[]; + attributes?: Attribute[]; - /** The events that this element fires. */ - events?: EventDoc[]; + /** + * The events that this element fires. + */ + events?: Event[]; /** * The shadow dom content slots that this element accepts. */ - slots?: SlotDoc[]; + slots?: Slot[]; - cssProperties?: CSSPropertyDoc[]; + parts?: CssPart[]; - cssParts?: CSSPartDoc[]; + cssProperties?: CssCustomProperty[]; demos?: Demo[]; } -export interface CustomElementDefinitionDoc { - kind: "definition"; - +export interface Attribute { name: string; - declaration: Reference; -} - -export interface AttributeDoc { - name: string; + /** + * A markdown summary suitable for display in a listing. + */ + summary?: string; /** - * A markdown description for the attribute. + * A markdown description. */ description?: string; + inheritedFrom?: Reference; + /** * The type that the attribute will be serialized/deserialized as. */ - type?: string; + type?: Type; /** * The default value of the attribute, if any. @@ -105,92 +227,123 @@ export interface AttributeDoc { * The name of the field this attribute is associated with, if any. */ fieldName?: string; - - /** - * A reference to the class or mixin that declared this property. - */ - inheritedFrom?: Reference; } -export interface EventDoc { +export interface Event { name: string; /** - * A markdown description of the event. + * A markdown summary suitable for display in a listing. */ - description?: string; + summary?: string; /** - * The type of the event object that's fired. - * - * If the event type is built-in, this is a string, e.g. `Event`, - * `CustomEvent`, `KeyboardEvent`. If the event type is an event class defined - * in a module, the reference to it. + * A markdown description. */ - type?: Reference | string; + description?: string; /** - * If the event is a CustomEvent, the type of `detail` field. + * The type of the event object that's fired. */ - detailType?: string; + type: Type; - /** - * A reference to the class or mixin that declared this property. - */ inheritedFrom?: Reference; } -export interface SlotDoc { +export interface Slot { /** * The slot name, or the empty string for an unnamed slot. */ name: string; /** - * A markdown description of the slot. + * A markdown summary suitable for display in a listing. */ - description?: string; + summary?: string; /** - * A reference to the class or mixin that declared this property. + * A markdown description. */ - inheritedFrom?: Reference; + description?: string; } -export interface CSSPropertyDoc { +/** + * The description of a CSS Part + */ +export interface CssPart { name: string; - description?: string; - type?: string; - default?: string; /** - * A reference to the class or mixin that declared this property. + * A markdown summary suitable for display in a listing. */ - inheritedFrom?: Reference; + summary?: string; + + /** + * A markdown description. + */ + description?: string; } -export interface CSSPartDoc { +export interface CssCustomProperty { + /** + * The name of the property, including leading `--`. + */ name: string; - description?: string; + + defaultValue?: string; /** - * A reference to the class or mixin that declared this property. + * A markdown summary suitable for display in a listing. */ - inheritedFrom?: Reference; + summary?: string; + + /** + * A markdown description. + */ + description?: string; } -export interface ClassDoc { - kind: "class"; +export interface Type { + /** + * The full string representation of the type, in whatever type syntax is + * used, such as JSDoc, Closure, or TypeScript. + */ + type: string; /** - * The class name, or `undefined` if the class is anonymous. + * An array of references to the types in the type string. + * + * These references have optional indices into the type string so that tools + * can understand the references in the type string independently of the type + * system and syntax. For example, a documentation viewer could display the + * type `Array` with cross-references to `FooElement` + * and `BarElement` without understanding arrays, generics, or union types. */ - name?: string; + references?: TypeReference[]; +} + +/** + * A reference that is associated with a type string and optionally a range + * within the string. + * + * Start and end must both be present or not present. If they're present, they + * are indices into the associated type string. If they are missing, the entire + * type string is the symbol referenced and the name should match the type + * string. + */ +export interface TypeReference extends Reference { + start?: number; + end?: number; +} + +/** + * The common interface of classes and mixins. + */ +export interface ClassLike { + name: string; /** * A markdown summary suitable for display in a listing. - * TODO: restrictions on markdown/markup. ie, no headings, only inline - * formatting? */ summary?: string; @@ -203,17 +356,21 @@ export interface ClassDoc { members?: Array; } -export type ClassMember = FieldDoc | MethodDoc; +export interface ClassDeclaration extends ClassLike { + kind: "class"; +} + +export type ClassMember = ClassField | ClassMethod; -export interface FieldDoc { - kind: "field"; +/** + * The common interface of variables, class fields, and function + * parameters. + */ +export interface PropertyLike { name: string; - static?: boolean; /** * A markdown summary suitable for display in a listing. - * TODO: restrictions on markdown/markup. ie, no headings, only inline - * formatting? */ summary?: string; @@ -221,60 +378,46 @@ export interface FieldDoc { * A markdown description of the field. */ description?: string; - default?: string; // TODO: make this a Type type or a Reference - privacy?: Privacy; - type?: string; - /** - * A reference to the class or mixin that declared this property. - */ + type?: Type; + + default?: string; +} + +export interface ClassField extends PropertyLike { + kind: "field"; + static?: boolean; + privacy?: Privacy; inheritedFrom?: Reference; } -export interface MethodDoc extends FunctionLike { +export interface ClassMethod extends FunctionLike { kind: "method"; - static?: boolean; - - /** - * A reference to the class or mixin that declared this property. - */ + privacy?: Privacy; inheritedFrom?: Reference; } /** - * TODO: tighter definition of mixin: - * - Should it only accept a single argument? - * - Should it not extend ClassDoc so it doesn't has a superclass? - * - What's TypeScript's exact definition? + * */ -export interface MixinDoc extends ClassDoc {} +export interface MixinDeclaration extends ClassLike, FunctionLike { + kind: "mixin"; +} -export interface VariableDoc { +export interface VariableDeclaration extends PropertyLike { kind: "variable"; - - name: string; - - /** - * A markdown summary suitable for display in a listing. - */ - summary?: string; - - /** - * A markdown description of the class. - */ - description?: string; - type?: string; } -export interface FunctionDoc extends FunctionLike { +export interface FunctionDeclaration extends FunctionLike { kind: "function"; } -export interface Parameter { - name: string; - type?: string; - description?: string; +export interface Parameter extends PropertyLike { + /** + * Whether the parameter is optional. Undefined implies non-optional. + */ + optional?: boolean; } export interface FunctionLike { @@ -286,19 +429,16 @@ export interface FunctionLike { summary?: string; /** - * A markdown description of the class. + * A markdown description. */ description?: string; - parameters?: Array; + parameters?: Parameter[]; return?: { - type?: string; + type?: Type; description?: string; }; - - privacy?: Privacy; - type?: string; } export type Privacy = "public" | "private" | "protected";