Tsdoc: API Contract Tags

Created on 22 Mar 2018  路  12Comments  路  Source: microsoft/tsdoc

As I've review the TypeDoc and AEDoc supported tags, there emerged a special type of tag. These modifier tags override the TypeScript definition for external documentation purposes.

@readonly (supported by AEDoc)
@private, @protected, and @public (supported by TypeDoc, AEDoc has a different @public)

Should this pattern be expanded or discouraged? In what cases does it make sense to have documentation deviate from implementation?

general discussion tool scenario

Most helpful comment

@pgonzal - sorry I'm a bit late to this party. @aciccarello is spot on. We use dgeni to do our documentation generation, which by itself does very little.

The bulk of the processing is in dgeni-packages which includes a jsdoc package and also typescript package. The former parses out jsdoc tags (which are completely configurable on a project by project basis) and the latter parses TS source code and infers lots of properties that would otherwise need jsdoc tags.

On top of that we have project specific configuration in the angular project at https://github.com/angular/angular/tree/master/aio/tools/transforms.

The list of tags linked to above is the "standard" ones that that the jsdoc package supports but for our purposes we add in a bunch of extra ones via https://github.com/angular/angular/tree/master/aio/tools/transforms/angular-api-package/tag-defs and https://github.com/angular/angular/tree/master/aio/tools/transforms/content-package/tag-defs. Some of which might be out of date and unused 馃槺.

As @aciccarello points out, this is not the whole story because we can then modify docs before they are rendered by processors, which is what is happening in the computeStability processor for the stability flags.

Happy to provide more detail of all of this if it is helpful to anyone.

All 12 comments

@readonly

AEDoc's @readonly tag was introduced for a weird edge case like this:

export class Widget {
  private _name: string;

  get name(): string { 
    return this._name; 
  }
  /** @readonly */
  set name(value: string) {
    throw new Error('The "name" property is read-only');
  }
}

// JavaScript caller:
var w = new Widget();
w.name = 'oops';

Normally API Extractor documents a property as read-only based on the lack of a setter. However, in this case the setter is performing validation. The API was created with TypeScript, but it can also be consumed by pure-JavaScript scripts that aren't aware of the type system.

I believe this can probably be deprecated now. IIRC the evolution was like this:

  1. Originally w.name = 'oops' (without setter) would silently overwrite the property getter with a string field, which was terrible
  2. After some tech upgrades, instead the assignment would do nothing, which was less damaging, but still highly confusing
  3. Today with "use strict", I believe it throws an exception

So as long as all the modern stuff has the semantics from # 3, we probably can retire this particular tag.

Other tags already captured by the TypeScript language

What is the use case for @private, @protected, and @public? If it's just holdovers from JSDoc's non-TypeScript world, then perhaps TSDoc tools should strip these tags, and try to avoid introducing new semantics for them.

Tags that extend the TypeScript semantics

For API Extractor's scenarios which go beyond the TypeScript language (e.g. @internal, @alpha, @beta, @public), it might be useful to coordinate with other tools that want these concepts, to ensure that the tags have consistent semantics everywhere. I'm pretty open to changing the naming/semantics.

We have received a bunch of feature requests that already would require some improvements to our naming/semantics. Example requests:

  • I want to mark something as "internal" but only within my project. This way my unit tests can access it, but other libraries that access "internals" cannot.

  • When I expose my library internals, I want to control which libraries have access to it. Something like C#'s "InternalsVisibleTo" or C++'s "friend".

  • When I mark something as "internal" for other first-party projects to consume, I want a way to designate it as "internal-beta" until the API contract is stable.

These needs mainly arise in large ecosystems with hundreds of projects and many teams who are trying to formalize their code-sharing relationships and responsibility for breakages.

What is the use case for @private, @protected, and @public?

I think the use case is for overriding the TypeScript visibility for external libraries which seems to match the @internal use case.

// file-1.ts
class Foo {
  /**
   * I want this to be easily accessible in my code but not used by library users
   * @protected
   */
  public bar
}

It sounds like the recommendation should be to avoid tags like these (including @readonly) that override the native TypeScript semantics.

Standardizing the release tags (or "API Contract" tags?) makes sense. The Angular team uses @experimental and @stable tags. If you look at their API docs they also handle @depricated and a special @security tag

Having default visibility for external libraries inferred from TypeScript modifiers (private, public, protected) seems way better than explicitly writing tags for every item.

But at the same time, if you need to override the visibility explicitly, it would be nice if you could do that.

What is a bit unclear to me is how do be disambiguate a situation like this:

When I expose my library internals, I want to control which libraries have access to it. Something like C#'s "InternalsVisibleTo" or C++'s "friend".

It seems that documentation of visibility might become very cluttered/verbose if someone has 2-3 parties with different visibilities of API.

On the other hand, if you really need to have different visibilities for different parties, you'd be happier having ability to specify that rather than having to maintain multiple d.ts'es or solve this in some hacky way.

So, if we're thinking of specifying party-specific API contract tags, we're better doing that from the start.

Because visibility is not runtime enforceable, I think these modifiers would be purely suggestive. The user would determine whether they should be accessing @internal or @beta apis just as someone would choose whether they are okay with using a @depricated api.

All of the tags here are standalone tags, i.e. whose presence acts as a simple on/off switch. They could probably be moved anywhere within the doc comment without ambiguity.

The one exception is @deprecated. For AEDoc, we require that @deprecated is required by a nonempty message that indicates what should be used instead. For example:

/**
 * The base class for all widgets.
 * @deprecated The BaseWidget class has been superseded by the BaseControl class.
 * @public
 */
class BaseWidget { 
  ...
}

This would generate a documentation banner like this:

! This API is now obsolete. The BaseWidget class has been superseded by the BaseControl class.

@aciccarello wrote:

Standardizing the release tags (or "API Contract" tags?) makes sense. The Angular team uses @experimental and @stable tags. If you look at their API docs they also handle @depricated and a special @security tag

Do you happen to know the name (or GitHub project) for Angular's API documentation tool? I am trying to cite it as an example. I found this api-builder folder, but it seems obsolete.

I think the config is now somewhere in angular/angular/aio/tools. They have some build setup based on angular/dgeni.

@petebacondarwin could probably explain things better.

Also if there's a listing somewhere of their supported tags, that would be useful. I'm having trouble finding example usages of the @stable tag.

Thanks very much for digging this up!

@pgonzal - sorry I'm a bit late to this party. @aciccarello is spot on. We use dgeni to do our documentation generation, which by itself does very little.

The bulk of the processing is in dgeni-packages which includes a jsdoc package and also typescript package. The former parses out jsdoc tags (which are completely configurable on a project by project basis) and the latter parses TS source code and infers lots of properties that would otherwise need jsdoc tags.

On top of that we have project specific configuration in the angular project at https://github.com/angular/angular/tree/master/aio/tools/transforms.

The list of tags linked to above is the "standard" ones that that the jsdoc package supports but for our purposes we add in a bunch of extra ones via https://github.com/angular/angular/tree/master/aio/tools/transforms/angular-api-package/tag-defs and https://github.com/angular/angular/tree/master/aio/tools/transforms/content-package/tag-defs. Some of which might be out of date and unused 馃槺.

As @aciccarello points out, this is not the whole story because we can then modify docs before they are rendered by processors, which is what is happening in the computeStability processor for the stability flags.

Happy to provide more detail of all of this if it is helpful to anyone.

Thanks very much for providing this detail! I am almost done converting API Extractor to use the TSDoc library prototype, and then my next step will be to audit the various other documentation tools that people have provided links to, and start an initial draft of a standards doc proposing detailed syntaxes for all the tags.

There's a preliminary sketch in StandardTags.ts.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

octogonz picture octogonz  路  7Comments

octogonz picture octogonz  路  8Comments

iansan5653 picture iansan5653  路  8Comments

AndrewCraswell picture AndrewCraswell  路  7Comments

wereHamster picture wereHamster  路  6Comments