Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Syntax for "declaration references" (hyperlinks) #9

Open
octogonz opened this issue Mar 22, 2018 · 57 comments
Open

RFC: Syntax for "declaration references" (hyperlinks) #9

octogonz opened this issue Mar 22, 2018 · 57 comments
Labels
request for comments A proposed addition to the TSDoc spec tool scenario Includes a writeup about a tool that might use TSDoc

Comments

@octogonz
Copy link
Collaborator

Certain tags such as {@inheritdoc} contain references to other API items. AEDoc supports two notations for API item references:

  • For internal references, we use: exportName.memberName

    Example: {@inheritdoc BaseWidget.draw}

    The ".memberName" is optional and included e.g. if the API item is a member of a class or interface.

  • For external references, we use: @scopeName/packageName:exportName.memberName

    Example: {@inheritdoc @microsoft/widget-lib:BaseWidget.draw}

    The "@scopeName/" and ".memberName" are optional. The "@scopeName/" is used for scoped NPM packages.

This might be a reasonable starting point. However, it doesn't consider nesting members such as namespaces (which has turned out to be surprisingly common for legacy SDKs, despite all the downsides and attempts to deprecate them).

Also, AEDoc today overlooks an interesting edge case:

export class A {
    public b: string = 'hello';
}

export namespace A {
    export let b: number = 123;
}

let a = new A();

// a.b is a string
// A.b is a number

In this example, an expression such as {@link A.b} would be ambiguous. Which declaration is it referring to? How to handle this?

@octogonz octogonz added the request for comments A proposed addition to the TSDoc spec label Mar 22, 2018
@DovydasNavickas
Copy link

Me and @MartynasZilinskas have a solution to this. We'll post more info a bit later today / tomorrow. We also have it working in ts-extractor's rewritten version according to what we talked about privately with you (ids, aforementioned ambiguity for same name, different types and not requiring JSON output). We also seem to have solved "declarations vs symbols" thingy and already extract both and also deal with them ids-wise.

P.S. You called hyperlinks what we call ids, but I'd rather we call them ids or identifiers, because we can use them in various situations, not only hyperlinks and they have to be unique and point to something specific, which is identifier.

@aciccarello
Copy link

How should internal references be looked up? TypeDoc isn't limited to a single API export so you could have multiple BaseWidgets that could match. Imports could give you a hint but I don't think we want the documentation parser to have to read those and it wouldn't work in all cases.

@octogonz octogonz changed the title RFC: Syntax for hyperlinking to API definitions RFC: Syntax for API item references (hyperlinks) Mar 23, 2018
@octogonz
Copy link
Collaborator Author

octogonz commented Mar 23, 2018

@DovydasNavickas I've retitled this from "hyperlinks" to "API item references." This clarifies that we're not just talking about clickable hyperlinks. The name "ids" seemed a little too vague.

How should internal references be looked up? TypeDoc isn't limited to a single API export
so you could have multiple BaseWidgets that could match.

Option 1: Require the names to be exported

Today API Extractor only allows references to exported items, using the official exported name. Today it doesn't support multiple entry points, but it would be fairly straightforward.

For example, if an API consumer has to write this:

import { Button } from '@scope/my-library/ui/controls';

Then the {@link} tag for an external consumer could look like this:

/**
 * {@link @scope/my-library/ui/controls:Button.render | the render() method}
 */

So for a local reference inside of @scope/my-library, we might expect it to disambiguate the entry point like this:

/**
 * {@link /ui/controls:Button.render | the render() method}
 */

Note that the TSDoc layer would not be directly involved in this. The @microsoft/tsdoc library would simply provide a standard notation and parser, which might return an AST node like this:

// {@link @scope/my-library/ui/controls:Button.render | the render() method}

{
  "nodeKind": "linkNode",
  "apiItemReference": {
    "scope": "@scope",
    "library": "my-library",
    "modulePath": "/ui/controls",
    "itemPath": "Button.render"
  },
  "linkText": {
    "nodeKind": "textNode",
    "content": "the render() method"
  }
}

Option 2: Rely on the compiler symbols

Another option would be to rely on the compiler symbols. This is fairly straightforward with today's compiler API, but only if the symbol is actually imported. Example:

import { Button as RenamedForSomeReason } from '../../ui/controls';

/**
 * {@link RenamedForSomeReason.render | the render() method}
 */

@MartynasZilinskas
Copy link
Contributor

// index.ts file in `example-package`

// example-package/index:Foo#class
class Foo {
    // example-package/index:Foo#class.a#property
    public a: string;
}

// example-package/index:Foo#namespace
namespace Foo {
    // example-package/index:Foo#class.a#variable
    const a: number;
}

We ugpraded the format for specifying api item.
{package-name}/{path-to-file}:{selector}#{kind}

I am thinking of adding an additional ApiItem symbol. It will respresent all declarations behind that symbol.
For example:

"example-package/index:Foo": {
    "members": [
        "example-package/index:Foo#class", 
        "example-package/index:Foo#namespace"
    ]
}

When resolving id without specific kind part and that symbol has only single declaration, than Symbol ApiItem will act as alias to that single declaration.

// `example-package` at index.ts
namespace Foo {
    const a: number;
}

class Foo {
    /**
     * Property `a`.
     */
    public a: string;
}

// `other-package`
class Bar extends Bar{
    /**
     * {@inheritdoc example-package/index:Foo#class.a}
     */
    public a: string;
}

Specifying kind for Foo is mandatory because we have symbol that has two declarations.

The only problem I see is overloads, because they have same name and kinds.

P.S. Working delimiters !@#%^&*)+?;,..

@octogonz
Copy link
Collaborator Author

I like this idea of using # to disambiguate merged declarations. I was originally considering proposing something like :: vs . from C++ to distinguish static versus non-static members. But it would still leave open the question of how to refer to Foo as a namespace versus Foo as a class, an API reference site would probably want separate manual pages for these manifestations.

But I don't think you would ever need the #variable part. The a is unambiguous in example-package/index:Foo#class.a.

Property getters/setters are another example of multiple declarations for the same symbol, but I feel fairly strongly they should be treated as a single API item.

Overloads are an interesting case. C# /// comments would use a notation like {@link Widget.doSomething(string, string)} but that requires a full type system to resolve, and could be very verbose in TypeScript.

However, TypeScript requires all of the overloaded signatures to be right next to each other. They cannot be merged from different files. Also the ordering matters (since it determines matching precedence). So for this case, perhaps we could simply number them?

class Widget {

  // This is {@link Widget.doSomething#1}
  doSomething(name: string, age: number): void;

  // This is {@link Widget.doSomething#2}
  doSomething(options: { name: string, age: number }): void;

  // This is {@link Widget.doSomething#3}
  doSomething(): void;

  // This is the private implementation
  doSomething(whatever?: any, age?: number): void {
  }

}

It's not super elegant... but then neither are overloads. We don't use overloads all that much since the reflection check has a perf overhead, and the behavior gets weird with subclassing and such. It is a half-baked feature compared to languages such as Java or C++. API Extractor doesn't even give them separate manual pages; we treat overloads as a single function with a complicated type signature.

For people who really care about this, a more elaborate solution might be to provide a way to create named monikers. Something like this:

class Widget {

  /** @overload nameAndAge */
  doSomething(name: string, age: number): void;
  /** @overload options */
  doSomething(options: { name: string, age: number }): void;
  /** @overload none */
  doSomething(): void;

  doSomething(junk?: any, age?: number): void {
  }

}

/** The {@link Widget.doSomething#nameAndAge} is the first overload.

Any better ideas?

@MartynasZilinskas
Copy link
Contributor

I upgraded ts-extractor to use this format of ids for ast items.

{package-name}/{path-to-file}:{selector}#{kind}&{itemCounter}

Code sample:

export function pickCard(x: { suit: string; card: number }[]): number;
export function pickCard(x: number): { suit: string; card: number };
export function pickCard(x: any): number | { suit: string; card: number } | undefined {
    const suits = ["hearts", "spades", "clubs", "diamonds"];
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    } else if (typeof x == "number") {
        // Otherwise just let them pick the card
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

This how ast item ids look like under the hood.

@simplrjs/package-name/index.ts // SourceFile
@simplrjs/package-name/index.ts:pickCard // AstSymbol
@simplrjs/package-name/index.ts:pickCard#function&1 // AstFunction
@simplrjs/package-name/index.ts:pickCard#function&1.x // AstSymbol
@simplrjs/package-name/index.ts:pickCard#function&1.x#parameter&1 // AstParameter
@simplrjs/package-name/index.ts:pickCard#function&2 // AstFunction
@simplrjs/package-name/index.ts:pickCard#function&2.x // AstSymbol
@simplrjs/package-name/index.ts:pickCard#function&2.x#parameter&1 // AstParameter
@simplrjs/package-name/index.ts:pickCard#function&3 // AstFunction
@simplrjs/package-name/index.ts:pickCard#function&3.x // AstSymbol
@simplrjs/package-name/index.ts:pickCard#function&3.x#parameter&1 // AstParameter

We are using a flat Map for items registry, so items ids need to be unique.

But I don't think you would ever need the #variable part. The a is unambiguous in example-package/index:Foo#class.a.

If id referencing to a symbol and it only has 1 declaration, then you don't need to specify #{kind} part.

Simple example

/**
 * Says Hello world!
 */
export const a: string = "Hello World!";

/**
 * {@inheritdoc @simplrjs/package-name/index.ts:a}
 */
export const b: string = "-";

Example

export namespace A.B.C.D.E {
    /**
     * Says Hello world!
     */
    export const a: string = "Hello World!";
}

/**
 * {@inheritdoc @simplrjs/package-name/index.ts:A.B.C.D.E.a}
 */
export const b: string = "-";

However, TypeScript requires all of the overloaded signatures to be right next to each other. They cannot be merged from different files. Also the ordering matters (since it determines matching precedence). So for this case, perhaps we could simply number them?

If you need to inherit documentation from a specific overload, numbering is the way to go.

@DovydasNavickas
Copy link

@pgonzal said:

But I don't think you would ever need the #variable part. The a is unambiguous in example-package/index:Foo#class.a.

@MartynasZilinskas said:

If id is referencing a symbol and it only has 1 declaration, then you don't need to specify the #{kind} part.

I think we have situation, where we have two different goals:

  • Registry ids have to be unique, therefore they have to be as verbose as they can get.

But:

  • API item references should be more loose for convenience and could omit some of the parts, if one is sure that they are optional.

What I'm a bit more careful about is having those optional parts omitted and reference becoming ambiguous later on in the development, when say another declaration is added to the symbol (e.g. inherit doc or a link gets ambiguous).

On the other hand, that might even become a perk.
If the reference becomes ambiguous later on, you might need to update the reference to a more specific one either way, so this could be a good signal that you really need to take a look into that older part and specify what exactly are you referring to.

Thoughts?

@yume-chan
Copy link

For the overload part, I strongly disagree using magic numbers to label them. If the performance is a concern, the doc generator may produce extra metadata to help distinguish them (by index, for example) for other tools, but the developer must have the ability to reference overloads by their signatures.

The #{type} syntax looks strange, how about just (class)Foo.(property)a? I think a pair of parentheses may not cause syntactic ambiguities. Or maybe <class>Foo.<property>a like a TypeScript cast operator.

@MartynasZilinskas
Copy link
Contributor

@yume-chan The concern is not with performance, but how verbose can it become, like @pgonzal pointed out. The order of overload signatures matters, so numbering them makes sense.

Consider this example:

class Widget {
    doSomething(name: string, age: number): void; // Widget.doSomething(string, number)
    doSomething(options: { name: string; age: number }): void; // Widget.doSomething({ name: string; age: number })    
    doSomething(): void; // Widget.doSomething()
}

How reference will look like for this overload?

doSomething(options: { name: string; age: number }): void;

I see the real problem when you have whole bunch of overloads (example). In this case, labeling would be convenient.

Usage:

@label <name>

Example:

class Widget {
    /**
     * Do something with name and age.
     * @label nameAndAge
     */
    doSomething(name: string, age: number): void; // Widget.doSomething#nameAndAge or Widget.doSomething&1
    doSomething(options: { name: string; age: number }): void; // Widget.doSomething&2
    /**
     * No arguments needed to do something.
     */
    doSomething(): void; // Widget.doSomething&3
}

class CustomWidget {
    /**
     * {@inheritdoc Widget.doSomething#nameAndAge}
     */
    doSomething(name: string, age: number): void;
    /**
     * {@inheritdoc Widget.doSomething&3}
     */
    doSomething(): void {}
}

All possible naming variants so far:

  • @label
  • @overload-label
  • @overload

What do you think about @label @pgonzal? I think it represents better what it means than @overload.


The #{type} syntax looks strange, how about just (class)Foo.(property)a? I think a pair of parentheses may not cause syntactic ambiguities. Or maybe <class>Foo.<property>a like a TypeScript cast operator.

The #{kind} you're referring to is not a type. The idea here is to specify which syntax kind it represents when more than one declaration is available under the same symbol name. Plus, in ts-extractor we want to use these references as unique identifiers in AST items registry.

@yume-chan
Copy link

yume-chan commented Mar 29, 2018

How reference will look like for this overload?

I now understand the problem is the complex type system in TypeScript. I came from C# and I have used to using signature to reference overloads.

labeling would be convenient.

I think label is a good idea.

The #{kind} you're referring to is not a type.

I know, I just used the word "type". But class is a function and namespace is an object, they are still "types", aren't them? Anyway, I suggest a new syntax to distinguish different node kinds by a TypeScript style type conversion operator. It's more familiar with TypeScript programmers and because the kind is in the front of name, it's more easily to read and understand.

@DovydasNavickas
Copy link

But class is a function and namespace is an object, they are still "types", aren't them?

Well, in TypeScript's realm they are declarations, not types. And their common symbol is what binds them.

I suggest a new syntax to distinguish different node kinds by a TypeScript style type conversion operator.

I think this might be a confusing analogy. Also, if we write (class)Foo and (namespace)Foo, it is in the wrong order semantically, because a symbol Foo might have two declarations: class and namespace.
And therefore this disambiguation should be specified later in the reference:

Foo(class)
Foo(namespace)

But then it looks weird for me as it resembles a function call.
And # is something that specifies things in urls and is more likely to be understood more naturally:

Foo#class
Foo#namespace

And if you define reference for property myProp in a class Foo, it would look:

Foo#class.myProp

Which goes as Foo#class and dot-notation into its members. If we look at Foo#class as a single unit and for the sake of argument name it as FooClass, this reference would become:

FooClass.myProp

And this notation is more than familiar for anyone having done programming.

Also, if we adopt the @label tag, we can forbid reserved keywords usage for labels, such as class and namespace, but let people use other strings without spaces, e.g. magical-overload. Then we could use the same # notation with labels for code:

class Foo {
   public myProp: string;
    /**
     * @label magical-overload
     */
   public myMethod(a: string): void;
   public myMethod(): void;
}

This would be the references:

Foo#class.myProp                    // <-- Property
Foo#class.myMethod#magical-overload // <-- Specific overload with a label `magical-overload`

@octogonz
Copy link
Collaborator Author

@DovydasNavickas how would your proposed notation handle these two toString methods?

class Vector {
  public x: number;
  public y: number;

  public toString(): string {
    return `${this.x}, ${this.y}`;
  }

  public static toString(x: string, y: string): string {
    return `${x}, ${y}`;
  }
}

@MartynasZilinskas
Copy link
Contributor

We discussed with @DovydasNavickas offline about handling static members.

#{kind/label} format is no longer sufficient to specify only TypeScript syntax kinds and JSDoc @label tags.
We think of allowing multiple keywords that could be added to the symbol name to specify all required details: #{kind/label/scope}.

// Package: simple-package

class Vector {
    public x: number;
    public y: number;

    // Full id: simple-package:Vector#Class.toString#ClassMethod
    public toString(): string {
        return `${this.x}, ${this.y}`;
    }

    // Full id: simple-package:Vector#Class.toString#Static#ClassMethod
    public static toString(x: string, y: string): string {
        return `${x}, ${y}`;
    }
}

class Vector2 {
    /**
     * Used relative id.
     * @inheritDoc Vector.toString#Static#ClassMethod
     */
    public static toString(x: string, y: string): string {
        return `${x}, ${y}`;
    }
}

Static method toString has two keywords:

  • #Static- indicates this declaration is static
  • #ClassMethod - declaration kind

In this case, we need to specify declaration's syntax kind, because otherwise, we won't know if developer might've made a mistake.

class Vector {
    public x: number;
    public y: number;

    // Full id: simple-package:Vector#Class.toString#ClassMethod
    public toString(): string {
        return `${this.x}, ${this.y}`;
    }

    // [2] Full id: simple-package:Vector#Class.toString#Static#ClassMethod
    public static toString(x: string, y: string): string {
        return `${x}, ${y}`;
    }
}

class Vector2 {
    /**
     * [1] Used relative id.
     * @inheritDoc Vector.toString 
     */
    public toString(): string {
        return `${this.x}, ${this.y}`;
    }
}

Should we throw an error about ambiguous identifier on [1] when the static method with the same name is later added at [2]?

This is the only shortcoming that we see at the moment.

@MartynasZilinskas
Copy link
Contributor

In the newest prototype of ts-extractor, I encountered a few problems while generating AstItem's ids. I got more mature ideas about id format to be used.

The format that will be used in linking declarations doesn't change.
{package-name}/{path-to-file}:{selector}:{types}

There will be 3 types of separators:

  • [:] semicolon separates file location, declaration selector and its types
  • [.] dot separates hierarchical part of id (e.g. symbols, declarations and types)
  • [#] hashtag separates symbol and scope (#Static), kind (#Namespace, #Class), label (#this-label, #another-label) and unique counter for unlabeled overloads (#1, #2)

[:] Semicolon separator

Separator between location, declaration selector and type.

// Package: "package"
// File: "components.ts"

// Symbol: "TextField"
// Declaration: "Class"
export class TextField {}
Id:
package/components:TextField#Class

Parts:
package/components : TextField#Class
{package}          : {declarations}     

Separator between declaration selector and type selector.

// Package: "package"
// File: "helpers.ts" 

// Symbol: "resolveId"
// Declaration: "Function"
// ReturnType: "TypeBasic" -> string
export function foo(a: any): string;
Id:
package/helpers:foo#Function:TypeBasic

Parts:
package/helpers : foo#Function  : TypeBasic
{package}       : {declaration} : {type}

This id refers to foo function return type. It will not be used in hyperlinks, because we don't need to point to anything more specific than actual function. Return type can also change more often than function name, which means previous hyperlinks would become incorrect.
To distinguish overloads, we'll use labels, but more on that later.

[.] Dot separator

Separate symbols

// Package: "package"
// File: "helpers.ts" 

// Symbol: "ComponentHelpers"
// Declaration: "Namespace"
export namespace ComponentHelpers {

    // Symbol: "foo"
    // Declaration: "Function"
    export function foo(a: any): string;
}
Id:
package/helpers:ComponentHelpers#Namespace.foo#Function

Parts:
{parent}                   . {child}
ComponentHelpers#Namespace . foo#Function

Separate types

// Package: "package"
// File: "helpers.ts" 

// Symbol: "Foo"
// Declaration: "TypeAlias"
// Type: "UnionType" -> `string | number`
export type Foo = string | number;
Id:
package/helpers:Foo#TypeAlias:UnionType.TypeBasic#1

Parts:
{parent}   .   {child}
UnionType  .   TypeBasic#1

[#] Hashtag separator

We can use different casings to distinguish hashtags:

  • scope, kind - PascalCase
  • counter - number
  • label - kebab-case

Ordering could also be strict:

  • #{kind}
  • #{kind}#{counter}
  • #{scope}#{kind}#{counter}

Simple example

// Package: "package"
// File: "components.ts"

// Symbol: "Foo"
// Declaration: "Class"
export class Foo {}
Id:
package/components:Foo#Class

Parts:
Foo      #Class
{name}   #{kind}

With static scope

// Package: "package"
// File: "components.ts"

// Symbol: "Foo"
// Declaration: "Class"
export class Foo {
    // Symbol: "bar"
    // Scope: "Static"
    // Declaration: "ClassProperty"
    public static bar: string;
}
Id:
package/components:Foo#Class.bar#Static#ClassProperty

Parts:
bar     #Static    #ClassProperty
{name}  #{scope}   #{kind}     

With Static scope and method overloads

// Package: "package"
// File: "components.ts"

// Symbol: "Foo"
// Declaration: "Class"
export class Foo {
    // Symbol: "bar"
    // Scope: "Static"
    // Declaration: "ClassProperty"
    // Counter: 0
    public static bar(x: string): string;    
    // Symbol: "bar"
    // Scope: "Static"
    // Declaration: "ClassProperty"
    // Counter: 1
    public static bar(): string;
}
Id:
package/components:Foo#Class.bar#Static#ClassMethod#0

Parts:
bar     #Static    #ClassMethod    #0  
{name}  #{scope}   #{kind}         #{counter}

Overload with a label

// Package: "package"
// File: "components.ts"

// Symbol: "Foo"
// Declaration: "Class"
export class Foo {
    // Symbol: "bar"
    // Scope: "Static"
    // Declaration: "ClassProperty"
    // Counter: 0
    public static bar(x: string): string;    
    // Symbol: "bar"
    // Scope: "Static"
    // Declaration: "ClassProperty"
    // Counter: 1
    // Label: "simple-label"
    /**
     * @label simple-label
     */
    public static bar(): string;
}
Id:
package/components:Foo#Class.bar#simple-label

Parts:
bar     #simple-label  
{name}  #{label}

@octogonz
Copy link
Collaborator Author

octogonz commented May 26, 2018

I find the notation here to be a little counterintuitive:

package/components:Foo#Class.bar#simple-label

Since we generally use InitialCaps for class names, the #Class part seems like an identifier rather than a system-defined kind. Also, since keywords like "class" or "static" normally precede the identifier in TypeScript declarations, in the expression Foo#Class.bar it seems like bar would be the class. We would ideally want the notation to feel like a suffix. The URL hashtag notation normally acts that way, but that sense might be lost in the middle of a big chain of symbols.

Here's a possible alternative since everyone expects [ ] to be a suffix operator:

@scope/my-package/components:Foo[class].member[static]
@scope/my-package/components:Foo[class].member[static,1]
@scope/my-package/components:Foo[class].member[static,SimpleLabel]

Ordering could also be strict:

  • #{kind}
  • #{kind}#{counter}
  • #{scope}#{kind}#{counter}

For the labels, if the developer is already doing the work to define labels to facilitate documentation generation, then they should be able to ensure that their labels are unique. Thus perhaps we would never need multiple hashtags? For example:

@scope/my-package/components:Foo[class].member[static]
@scope/my-package/components:Foo[class].member[static#1]
@scope/my-package/components:Foo[class].member[SimpleLabel]

@MartynasZilinskas
Copy link
Contributor

I agree with using [ ] brackets.

@yume-chan
Copy link

Here's a possible alternative since everyone expects [ ] to be a suffix operator:

But what if the class does include a field called "class" or something not a valid JavaScript identifier?

@octogonz
Copy link
Collaborator Author

But what if the class does include a field called "class" or something not a valid JavaScript identifier?

For that case, I suppose quotation marks would be required and could disambiguate it:

class Example {
  public 'class': number;

  /**
   * Assigns the {@link Example['class']} member.
   */
  public assignClass() {
    this['class'] = 123;
  }
}

But is this a realistic documentation scenario?

Personally I have a clear boundary between "strange constructs that we sometimes need to get the job done" versus "typical constructs that I'd bother documenting". I'm open to other perspectives/counterexamples, though.

@yume-chan
Copy link

yume-chan commented May 29, 2018

@pgonzal I think symbols will be a real use case, like Foo[Symbol.iterator] or some other user-defined symbols Foo[mySymbol].

And in my opinion, Foo[class] and Foo["class"] are still too similar and might cause problems for a user first see it.

@octogonz
Copy link
Collaborator Author

octogonz commented Jul 2, 2018

BTW I noticed that JSDoc calls this concept "namepaths": http://usejsdoc.org/about-namepaths.html

They use . for static members and # for non-static members, but that design seems a little underdeveloped for all the problems Martynas was tackling in his proposal above.

@dend
Copy link

dend commented Jul 9, 2018

In DocFX, we leverage the concept of UIDs - it would be interesting to see if that is something that we can plug in here as well, to make sure that writers/devs can easily leverage existing conventions.

@octogonz octogonz added the tool scenario Includes a writeup about a tool that might use TSDoc label Sep 1, 2018
@octogonz
Copy link
Collaborator Author

@MartynasZilinskas I am at the point where I need to implement a parser for the syntax discussed above. I'm going to make it part of the TSDoc library and spec. It sounds like we landed on essentially this:

@scope/my-package/components:Foo[class].member[static]
@scope/my-package/components:Foo[class].member[static#1]
@scope/my-package/components:Foo[class].member[SimpleLabel]

Did you have anything other corrections/enhancements that should be incorporated?

@octogonz
Copy link
Collaborator Author

In the newest prototype of ts-extractor, I encountered a few problems while generating AstItem's ids. I got more mature ideas about id format to be used.

BTW is this code that you could share? I looked around for it in the master branch but didn't see it.

@MartynasZilinskas
Copy link
Contributor

MartynasZilinskas commented Sep 15, 2018

@pgonzal I am developing on dev-next. It's a complete rewrite. Soon I will start implementing TSDoc.

Syntax looks good 👍 .

@octogonz
Copy link
Collaborator Author

After chatting with @RyanCavanaugh, we've decided to call this syntax a "declaration reference".

He expressed some concern about indexing based on "order of appearance in the source file", and favored the idea of defining explicit labels, such as the @overload tag above. He pointed out that the compiler's type matching for interfaces may not follow the order in which declarations appear.

@octogonz
Copy link
Collaborator Author

octogonz commented Feb 7, 2019

Update: microsoft/rushstack#881 (comment) led to an interesting discussion about how to name overloaded methods in the web site docs. This problem is closely related to the declaration reference selectors.

Let us know your opinion -- compare these 3 alternatives:

A. require a unique list of parameter names

calculate(calculationType)

function calculate(calculationType: CalculationType): void;

calculate(calculationTypeString)

function calculate(calculationTypeString: "Recalculate" | "Full"): void;

OR:

B. label selectors

calculate:ENUM

function calculate(calculationType: CalculationType): void;

calculate:STRING

function calculate(calculationType: "Recalculate" | "Full"): void;

OR:

C. index selectors

calculate:1

function calculate(calculationType: CalculationType): void;

calculate:2

function calculate(calculationType: "Recalculate" | "Full"): void;

 

 

Here's some screenshots @AlexJerabek shared that show a little more context:

overload_current

Having the types in the name would alleviate this problem (and look like .NET APIs, like this one).

overload_dotnet

@rix0rrr
Copy link

rix0rrr commented Mar 11, 2019

I just had the intent of linking to a package, like so:

{@link @aws-cdk/aws-lambda-event-sources}

But it seems not allowed to link to an entire package, I must link to an element INSIDE the package?

The declaration reference appears to contain a package name or import path, but it is missing the "#" delimiter
320    * Event sources are implemented in the {@link @aws-cdk/aws-lambda-event-sources} module.

@octogonz
Copy link
Collaborator Author

The syntax should be:

{@link @aws-cdk/aws-lambda-event-sources#}

I agree this feels a little counterintuitive. But imagine your package was called maximum. Then the notation {@link maximum} can be ambiguous: What if the current package also exported a function called maximum()? Which one is it referring to? The # makes it clear you're linking to an entire package.

@rix0rrr
Copy link

rix0rrr commented Mar 11, 2019

I see.Thanks for the explanation!

@Gerrit0
Copy link
Contributor

Gerrit0 commented May 13, 2019

Question - does the full name for interface members include :static?

The first example in the malformed names section includes it, other names do not:

* Full name: {@link (InterfaceJ1:interface).("abc. def":static)}

It seems like it might be a typo:

* NOTE: The full name doesn't have an additional selector, because interface
* members are unambiguous (except for operators and function overloads, which
* use labels).

@octogonz
Copy link
Collaborator Author

That is a typo. Interface members cannot be static. Thanks for pointing this out! Maybe you could open a PR to fix it? I am traveling this week.

@argv-minus-one
Copy link

According to the AEDoc documentation:

TSDoc declaration references are always resolved relative to a specific entry point (NOT relative to the current source file or declaration scope). Thus, their syntax is independent of where the reference occurs within a given package. Since Widget.initialize appears inside Widget, we may want to shorten the reference to {@link render | the render() method}, but TSDoc standard does not support this.

This behavior makes very little sense, is non-DRY, and is highly astonishing because it's completely unlike how TypeScript works. It will cause lots of developer confusion. The obvious expectation is that {@link Widget} links to whatever is named Widget in the lexical scope the comment appears in, not to some unrelated thing named Widget somewhere else in the package.

Please reconsider.

@Gerrit0
Copy link
Contributor

Gerrit0 commented Jun 4, 2019

I tend to disagree, TypeDoc currently uses the proposed behavior as it doesn't yet use TSDoc, and this makes it impossible to reference a global thing if you have a thing in your class.... this also results in TypeDoc spending roughly a quarter of the time spent documenting your code looking for references (yea, sure, there's definitely optimizations that could be done, but it is still going to be slower)

Linking based on package entry also helps make sure that my documentation doesn't change because I've imported some new function in a file (which might not even be public!)

@octogonz
Copy link
Collaborator Author

octogonz commented Jun 4, 2019

The obvious expectation is that {@link Widget} links to whatever is named Widget in the lexical scope the comment appears in, not to some unrelated thing named Widget somewhere else in the package.

@argv-minus-one I agree that this a somewhat obvious expectation. However, I don't know how we could realistically implement it without a lot of work.

For example, suppose I wrote something like this:

import Widget from 'widget-lib';

/**
 * Generates a new id for a {@link Widget}.
 */
export function generateWidgetId(): string;

This code produces an output .d.ts file that's missing the import statement, because the compiler thinks that nobody's using the Widget symbol in this file. That's a problem for API Extractor, as it analyzes the .d.ts file, not the .ts file. (There's some advantages to analyzing the API surface from the perspective of an external consumer.) Other tools may parse the .ts file, but they'll encounter the problem when they need to read TSDoc from an installed NPM dependency, since NPM packages typically install .d.ts files, not .ts files. Also, TSLint is going to whine about the unused import.

To address this, we could improve TypeScript and TSLint to understand the @link tag. Then they could parse the declaration reference and see that the Widget symbol is referenced. That solves @link. But here's a new problem: TSDoc is extensible. It allows declaration references to be used in custom inline tags that are tool-specific. To handle this, we need a setting somewhere (e.g. in package.json or tsdoc-metadata.json) that can define these custom tags. (That feature has been proposed already and is a good idea.)

Suppose we did all that stuff. There's some further consequences: API Extractor doesn't generate docs directly. Instead, it writes an .api.json file for each NPM package, so that API Documenter can come along and produce an integrated website with proper hyperlinking between all the packages. The .api.json stage also enables people to develop their packages in different Git repos. These JSON files contain the TSDoc and declaration references but don't have import statements, so they'd need to be rewritten with absolute references. API Extractor also writes a .d.ts rollup file, which does have import statements, but different ones from the local source files, so we'd want to rewrite those as absolute references as well. In short, wherever TSDoc moves between files, the file-relative references need to get rewritten. Most likely they need to get converted back to absolute references. (This implies that absolute references are a required feature, whereas file-relative references are optional.)

Okay but if we implemented all that stuff, file-relative would be doable, and it's probably a more intuitive for developers. By the time we got this working, there'd be a lot of legacy code using the old notation, so we maybe should define a special operator (let's say {@link ^Widget.render}) to indicate a reference to a local symbol. Sounds good to me! 😊

@argv-minus-one
Copy link

What about defining a syntax for declaration references in doc comments that's independent of TSDoc tags?

You might decide that anything surrounded by [[]] in a doc comment is taken to be a declaration reference with lexical scope, no matter where in the doc comment it appears. A TSDoc link would be written like [[Widget]], and any other tag that takes a declaration reference would also use this syntax, e.g. @throws [[BadWidgetError]].

TypeScript then knows that [[Widget]] is a reference to whatever is named Widget in that lexical scope, and therefore emits an import for it in the .d.ts, even if it is not otherwise needed. TypeScript doesn't need to know about tags, just declaration references.

There would need to be some other syntax to get different link text. The obvious choice is {@link [[Widget]] | different link text}, though another, terser option is making it resemble a Markdown reference-style link: [different link text][[Widget]]


I don't understand how the rollup issue applies only to declaration references, and not to rolling up declaration files in general. Suppose you have to roll this up:

// module1.d.ts

import { Widget } from "one-widget-package";
export declare class MyWidget extends Widget {  }
// module2.d.ts

import { Widget } from "another-widget-package";
export declare class MyOtherWidget extends Widget {  }

You can't just naïvely concatenate them:

// rollup.d.ts

import { Widget } from "one-widget-package";
export declare class MyWidget extends Widget {  }

import { Widget /* Name collision! */ } from "another-widget-package";
export declare class MyOtherWidget extends Widget /* Which Widget? */ {  }

Doesn't there already have to be some sort of disambiguation done to solve this problem? If so, why isn't that solution also applicable to declaration references? For example, if you disambiguate by renaming imports:

// module1.d.ts

import { Widget } from "one-widget-package";
/** It's a kind of [[Widget]]. */
export declare class MyWidget extends Widget {  }
// module2.d.ts

import { Widget } from "another-widget-package";
/** It's a different kind of [[Widget]]. */
export declare class MyOtherWidget extends Widget {  }
// rollup.d.ts

import { Widget } from "one-widget-package";
/** It's a kind of [[Widget]]. */
export declare class MyWidget extends Widget {  }

import { Widget as Widget2 } from "another-widget-package";
/** It's a different kind of [[Widget2]]. */
export declare class MyOtherWidget extends Widget2 {  }

@octogonz
Copy link
Collaborator Author

@rbuckton has been working on a simplification of the declaration syntax that also makes it more consistent with JSDoc namepaths.

The initial work is committed here:

And a bunch of examples can be found by searching for "canonicalReference" in PR microsoft/web-build-tools#1406 which updates API Extractor to generate this experimental notation in its .api.json output (without affecting the TSDoc input for now).

@hinell
Copy link

hinell commented Apr 29, 2020

[...] How to handle this?

Just link to disambiguation page and the problem is solved.

@octogonz
Copy link
Collaborator Author

export class A {
    public b: string = 'hello';
}

export namespace A {
    export let b: number = 123;
}

let a = new A();

// a.b is a string
// A.b is a number

[...] How to handle this?

Just link to disambiguation page and the problem is solved.

FYI @rbuckton's updated syntax uses different operators to disambiguate them:

  • A.b is the static number member.
  • A#b is the instance string member.

But where it is NOT ambiguous (for example because there is no static member), then we won't consider it an error to write A.b instead of A#b. This lenient policy should minimize the amount of migration work needed when we release the new syntax.

(Ron has made a lot of progress with overhauling the parser to integrate this new grammar, but it isn't a simple update. It will be a breaking change for the DocDeclarationReference / DocMemberReference parser APIs.)

@RA80533
Copy link

RA80533 commented Sep 1, 2020

Will the upcoming private fields readily follow such syntax?

  • Container.#id (private static)
  • Container##id (private instance)

Per sec. 4 of the proposal:

This proposal allows both abbreviated references to private state (#x) which have an implicit this as the receiver, as well as explicit receivers (obj.#x). The implicit receiver form is intended for better ergonomics for the common case, and the explicit receiver form allows the full generality of "class-private" (as opposed to "instance-private").

@fwienber
Copy link

Will the upcoming private fields readily follow such syntax?

Do you really think it is necessary to reference hard private Members in documentation? I think code comments as developer documentation should suffice in this case, especially since the referenced target must be declared in the same class in the same source code file and thus should be easy to find.
The syntax to reference instance members by # and static members by . is much older than the invention of a # prefix for private names in ECMAScript.
To me, the only important thing is that the parser must not get confused by the presence of private fields. It is perfectly valid to have a "normal" member and a private-name member of the same base name:

class Foo {
  foo = "";
  #foo = 123;
  /**
   * Updates the field {@link Foo#foo}, which is public and of type string.
   */
  updateFoo() { ... }
}

@GitMurf
Copy link

GitMurf commented Sep 29, 2023

Is the tsdoc project still being actively worked on? Seems like many items from the tsdoc website/spec reference things like this one that are not complete but have been open for several years now... for this one, five years! Does anyone know the status of the tsdoc project overall?

image

@octogonz
Copy link
Collaborator Author

@GitMurf The TSDoc project is still actively supported, but for some time there has not been much progress on new features. TSDoc is maintained by Rush Stack, along with API Extractor which relies very heavily on the TSDoc system. Unfortunately we have not be able to prioritize development of TSDoc because the current implementation already meets the needs of API Extractor very well. Every time TSDoc comes up, I want so badly to come work on it, but unfortunately some other project always seems to have higher priority.

If someone from the community could get involved and help push TSDoc along, that would be really great. 🙏 TSDoc is a relatively complex project however, so what we need most isn't small PRs, but rather someone with time to learn about the parser and requirements, and maybe help get some of the already existing PRs merged. (If anyone's interested, feel free to contact me.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request for comments A proposed addition to the TSDoc spec tool scenario Includes a writeup about a tool that might use TSDoc
Projects
None yet
Development

No branches or pull requests