Previously, it was necessary to attach on of the three "stability" jsdoc tags (`@stable`, `@deprecated` or `@experimental`) to each public API export. To ensure that the public API was correctly tagged, the `ts-api-guardian` would check that one of these tags appeared on every public export. Now the doc-gen is able to compute that a code item is stable if it does not contain the `@experimental` nor `@deprecated` tags. Therefore there is no need to provide the `@stable` tag any more; and this tag has now been marked as deprecated - i.e. it should not be used. The ts-api-guardian has been modified in this commit so that it no longer warns/fails if the `@stable` is missing. PR Close #23176
		
			
				
	
	
		
			506 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			506 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/**
 | 
						|
 * @license
 | 
						|
 * Copyright Google Inc. All Rights Reserved.
 | 
						|
 *
 | 
						|
 * Use of this source code is governed by an MIT-style license that can be
 | 
						|
 * found in the LICENSE file at https://angular.io/license
 | 
						|
 */
 | 
						|
 | 
						|
import * as chai from 'chai';
 | 
						|
import * as ts from 'typescript';
 | 
						|
 | 
						|
import {SerializationOptions, publicApiInternal} from '../lib/serializer';
 | 
						|
 | 
						|
const classesAndInterfaces = `
 | 
						|
  export declare class A {
 | 
						|
      field: string;
 | 
						|
      method(a: string): number;
 | 
						|
  }
 | 
						|
  export interface B {
 | 
						|
      field: A;
 | 
						|
  }
 | 
						|
  export declare class C {
 | 
						|
      someProp: string;
 | 
						|
      propWithDefault: number;
 | 
						|
      private privateProp;
 | 
						|
      protected protectedProp: number;
 | 
						|
      constructor(someProp: string, propWithDefault: number, privateProp: any, protectedProp: number);
 | 
						|
  }
 | 
						|
`;
 | 
						|
 | 
						|
describe('unit test', () => {
 | 
						|
  let _warn: any = null;
 | 
						|
  let warnings: string[] = [];
 | 
						|
  beforeEach(() => {
 | 
						|
    _warn = console.warn;
 | 
						|
    console.warn = (...args: string[]) => warnings.push(args.join(' '));
 | 
						|
  });
 | 
						|
 | 
						|
  afterEach(() => {
 | 
						|
    console.warn = _warn;
 | 
						|
    warnings = [];
 | 
						|
    _warn = null;
 | 
						|
  });
 | 
						|
 | 
						|
  it('should ignore private methods', () => {
 | 
						|
    const input = `
 | 
						|
      export declare class A {
 | 
						|
          fa(): void;
 | 
						|
          protected fb(): void;
 | 
						|
          private fc();
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A {
 | 
						|
          fa(): void;
 | 
						|
          protected fb(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should support overloads functions', () => {
 | 
						|
    const input = `
 | 
						|
      export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
 | 
						|
 | 
						|
      export declare function registerLocaleData(data: any, extraData?: any): void;
 | 
						|
      export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
 | 
						|
    `;
 | 
						|
 | 
						|
    const expected = `
 | 
						|
      export declare function group(steps: AnimationMetadata[], options?: AnimationOptions | null): AnimationGroupMetadata;
 | 
						|
 | 
						|
      export declare function registerLocaleData(data: any, extraData?: any): void;
 | 
						|
      export declare function registerLocaleData(data: any, localeId?: string, extraData?: any): void;
 | 
						|
    `;
 | 
						|
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should ignore private props', () => {
 | 
						|
    const input = `
 | 
						|
      export declare class A {
 | 
						|
          fa: any;
 | 
						|
          protected fb: any;
 | 
						|
          private fc;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A {
 | 
						|
          fa: any;
 | 
						|
          protected fb: any;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should support imports without capturing imports', () => {
 | 
						|
    const input = `
 | 
						|
      import {A} from './classes_and_interfaces';
 | 
						|
      export declare class C {
 | 
						|
          field: A;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class C {
 | 
						|
          field: A;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should throw on aliased reexports', () => {
 | 
						|
    const input = `
 | 
						|
      export { A as Apple } from './classes_and_interfaces';
 | 
						|
    `;
 | 
						|
    checkThrows(
 | 
						|
        {'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input},
 | 
						|
        'Symbol "A" was aliased as "Apple". Aliases are not supported.');
 | 
						|
  });
 | 
						|
 | 
						|
  it('should remove reexported external symbols', () => {
 | 
						|
    const input = `
 | 
						|
      export { Foo } from 'some-external-module-that-cannot-be-resolved';
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
    `;
 | 
						|
    check({'classes_and_interfaces.d.ts': classesAndInterfaces, 'file.d.ts': input}, expected);
 | 
						|
    chai.assert.deepEqual(
 | 
						|
        warnings, ['file.d.ts(1,1): error: No export declaration found for symbol "Foo"']);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort exports', () => {
 | 
						|
    const input = `
 | 
						|
      export declare type E = string;
 | 
						|
      export interface D {
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
      export declare var e: C;
 | 
						|
      export declare class C {
 | 
						|
          e: number;
 | 
						|
          d: string;
 | 
						|
      }
 | 
						|
      export declare function b(): boolean;
 | 
						|
      export declare const a: string;
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare const a: string;
 | 
						|
 | 
						|
      export declare function b(): boolean;
 | 
						|
 | 
						|
      export declare class C {
 | 
						|
          d: string;
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
 | 
						|
      export interface D {
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
 | 
						|
      export declare var e: C;
 | 
						|
 | 
						|
      export declare type E = string;
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort class members', () => {
 | 
						|
    const input = `
 | 
						|
      export class A {
 | 
						|
        f: number;
 | 
						|
        static foo(): void;
 | 
						|
        c: string;
 | 
						|
        static a: boolean;
 | 
						|
        constructor();
 | 
						|
        static bar(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export class A {
 | 
						|
        c: string;
 | 
						|
        f: number;
 | 
						|
        constructor();
 | 
						|
        static a: boolean;
 | 
						|
        static bar(): void;
 | 
						|
        static foo(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort interface members', () => {
 | 
						|
    const input = `
 | 
						|
      export interface A {
 | 
						|
        (): void;
 | 
						|
        [key: string]: any;
 | 
						|
        c(): void;
 | 
						|
        a: number;
 | 
						|
        new (): Object;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export interface A {
 | 
						|
        a: number;
 | 
						|
        (): void;
 | 
						|
        new (): Object;
 | 
						|
        [key: string]: any;
 | 
						|
        c(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort class members including readonly', () => {
 | 
						|
    const input = `
 | 
						|
        export declare class DebugNode {
 | 
						|
          private _debugContext;
 | 
						|
          nativeNode: any;
 | 
						|
          listeners: any[];
 | 
						|
          parent: any | null;
 | 
						|
          constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
 | 
						|
          readonly injector: any;
 | 
						|
          readonly componentInstance: any;
 | 
						|
          readonly context: any;
 | 
						|
          readonly references: {
 | 
						|
              [key: string]: any;
 | 
						|
          };
 | 
						|
          readonly providerTokens: any[];
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
        export declare class DebugNode {
 | 
						|
          readonly componentInstance: any;
 | 
						|
          readonly context: any;
 | 
						|
          readonly injector: any;
 | 
						|
          listeners: any[];
 | 
						|
          nativeNode: any;
 | 
						|
          parent: any | null;
 | 
						|
          readonly providerTokens: any[];
 | 
						|
          readonly references: {
 | 
						|
              [key: string]: any;
 | 
						|
          };
 | 
						|
          constructor(nativeNode: any, parent: DebugNode | null, _debugContext: any);
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort two call signatures', () => {
 | 
						|
    const input = `
 | 
						|
      export interface A {
 | 
						|
        (b: number): void;
 | 
						|
        (a: number): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export interface A {
 | 
						|
        (a: number): void;
 | 
						|
        (b: number): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should sort exports including re-exports', () => {
 | 
						|
    const submodule = `
 | 
						|
      export declare var e: C;
 | 
						|
      export declare class C {
 | 
						|
          e: number;
 | 
						|
          d: string;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const input = `
 | 
						|
      export * from './submodule';
 | 
						|
      export declare type E = string;
 | 
						|
      export interface D {
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
      export declare function b(): boolean;
 | 
						|
      export declare const a: string;
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare const a: string;
 | 
						|
 | 
						|
      export declare function b(): boolean;
 | 
						|
 | 
						|
      export declare class C {
 | 
						|
          d: string;
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
 | 
						|
      export interface D {
 | 
						|
          e: number;
 | 
						|
      }
 | 
						|
 | 
						|
      export declare var e: C;
 | 
						|
 | 
						|
      export declare type E = string;
 | 
						|
    `;
 | 
						|
    check({'submodule.d.ts': submodule, 'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should remove module comments', () => {
 | 
						|
    const input = `
 | 
						|
      /**
 | 
						|
       * An amazing module.
 | 
						|
       * @module
 | 
						|
       */
 | 
						|
      /**
 | 
						|
       * Foo function.
 | 
						|
       */
 | 
						|
      export declare function foo(): boolean;
 | 
						|
      export declare const bar: number;
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare const bar: number;
 | 
						|
 | 
						|
      export declare function foo(): boolean;
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should remove class and field comments', () => {
 | 
						|
    const input = `
 | 
						|
      /**
 | 
						|
       * Does something really cool.
 | 
						|
       */
 | 
						|
      export declare class A {
 | 
						|
          /**
 | 
						|
           * A very interesting getter.
 | 
						|
           */
 | 
						|
          b: string;
 | 
						|
          /**
 | 
						|
           * A very useful field.
 | 
						|
           */
 | 
						|
          name: string;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A {
 | 
						|
          b: string;
 | 
						|
          name: string;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should skip symbols matching specified pattern', () => {
 | 
						|
    const input = `
 | 
						|
      export const __a__: string;
 | 
						|
      export class B {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export class B {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected, {stripExportPattern: /^__.*/});
 | 
						|
  });
 | 
						|
 | 
						|
  it('should throw on using non-whitelisted module imports in expression position', () => {
 | 
						|
    const input = `
 | 
						|
      import * as foo from './foo';
 | 
						|
      export declare class A extends foo.A {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    checkThrows(
 | 
						|
        {'file.d.ts': input}, 'file.d.ts(2,32): error: Module identifier "foo" is not allowed. ' +
 | 
						|
            'Remove it from source or whitelist it via --allowModuleIdentifiers.');
 | 
						|
  });
 | 
						|
 | 
						|
  it('should throw on using non-whitelisted module imports in type position', () => {
 | 
						|
    const input = `
 | 
						|
      import * as foo from './foo';
 | 
						|
      export type A = foo.A;
 | 
						|
    `;
 | 
						|
    checkThrows(
 | 
						|
        {'file.d.ts': input}, 'file.d.ts(2,17): error: Module identifier "foo" is not allowed. ' +
 | 
						|
            'Remove it from source or whitelist it via --allowModuleIdentifiers.');
 | 
						|
  });
 | 
						|
 | 
						|
  it('should not throw on using whitelisted module imports', () => {
 | 
						|
    const input = `
 | 
						|
      import * as foo from './foo';
 | 
						|
      export declare class A extends foo.A {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A extends foo.A {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected, {allowModuleIdentifiers: ['foo']});
 | 
						|
  });
 | 
						|
 | 
						|
  it('should not throw if non-whitelisted module imports are not written', () => {
 | 
						|
    const input = `
 | 
						|
      import * as foo from './foo';
 | 
						|
      export declare class A {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A {
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should keep stability annotations of exports in docstrings', () => {
 | 
						|
    const input = `
 | 
						|
      /**
 | 
						|
       * @deprecated This is useless now
 | 
						|
       */
 | 
						|
      export declare class A {
 | 
						|
      }
 | 
						|
      /**
 | 
						|
       * @experimental
 | 
						|
       */
 | 
						|
      export declare const b: string;
 | 
						|
      /**
 | 
						|
       * @stable
 | 
						|
       */
 | 
						|
      export declare var c: number;
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      /** @deprecated */
 | 
						|
      export declare class A {
 | 
						|
      }
 | 
						|
 | 
						|
      /** @experimental */
 | 
						|
      export declare const b: string;
 | 
						|
 | 
						|
      /** @stable */
 | 
						|
      export declare var c: number;
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
 | 
						|
  it('should keep stability annotations of fields in docstrings', () => {
 | 
						|
    const input = `
 | 
						|
      export declare class A {
 | 
						|
        /**
 | 
						|
         * @stable
 | 
						|
         */
 | 
						|
        value: number;
 | 
						|
        /**
 | 
						|
         * @experimental
 | 
						|
         */
 | 
						|
        constructor();
 | 
						|
        /**
 | 
						|
         * @deprecated
 | 
						|
         */
 | 
						|
        foo(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    const expected = `
 | 
						|
      export declare class A {
 | 
						|
        /** @stable */ value: number;
 | 
						|
        /** @experimental */ constructor();
 | 
						|
        /** @deprecated */ foo(): void;
 | 
						|
      }
 | 
						|
    `;
 | 
						|
    check({'file.d.ts': input}, expected);
 | 
						|
  });
 | 
						|
});
 | 
						|
 | 
						|
function getMockHost(files: {[name: string]: string}): ts.CompilerHost {
 | 
						|
  return {
 | 
						|
    getSourceFile: (sourceName, languageVersion) => {
 | 
						|
      if (!files[sourceName]) return undefined;
 | 
						|
      return ts.createSourceFile(
 | 
						|
          sourceName, stripExtraIndentation(files[sourceName]), languageVersion, true);
 | 
						|
    },
 | 
						|
    writeFile: (name, text, writeByteOrderMark) => {},
 | 
						|
    fileExists: (filename) => !!files[filename],
 | 
						|
    readFile: (filename) => stripExtraIndentation(files[filename]),
 | 
						|
    getDefaultLibFileName: () => 'lib.ts',
 | 
						|
    useCaseSensitiveFileNames: () => true,
 | 
						|
    getCanonicalFileName: (filename) => filename,
 | 
						|
    getCurrentDirectory: () => './',
 | 
						|
    getNewLine: () => '\n',
 | 
						|
    getDirectories: () => []
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function check(
 | 
						|
    files: {[name: string]: string}, expected: string, options: SerializationOptions = {}) {
 | 
						|
  const actual = publicApiInternal(getMockHost(files), 'file.d.ts', {}, options);
 | 
						|
  chai.assert.equal(actual.trim(), stripExtraIndentation(expected).trim());
 | 
						|
}
 | 
						|
 | 
						|
function checkThrows(files: {[name: string]: string}, error: string) {
 | 
						|
  chai.assert.throws(() => { publicApiInternal(getMockHost(files), 'file.d.ts', {}); }, error);
 | 
						|
}
 | 
						|
 | 
						|
function stripExtraIndentation(text: string) {
 | 
						|
  let lines = text.split('\n');
 | 
						|
  // Ignore first and last new line
 | 
						|
  lines = lines.slice(1, lines.length - 1);
 | 
						|
  const commonIndent = lines.reduce((min, line) => {
 | 
						|
    const indent = /^( *)/.exec(line) ![1].length;
 | 
						|
    // Ignore empty line
 | 
						|
    return line.length ? Math.min(min, indent) : min;
 | 
						|
  }, text.length);
 | 
						|
 | 
						|
  return lines.map(line => line.substr(commonIndent)).join('\n') + '\n';
 | 
						|
}
 |