From 819664f5421f27e7fe51e4b758a601512d852654 Mon Sep 17 00:00:00 2001 From: monfera Date: Sun, 17 Feb 2019 20:41:37 +0100 Subject: [PATCH] Test: TypeScript type specification strength tests --- package.json | 2 + .../__fixtures__/typescript/typespec_tests.ts | 82 +++++++++++++++++++ .../canvas/public/lib/aeroelastic/dom.ts | 2 +- .../canvas/public/lib/aeroelastic/gestures.js | 2 +- .../lib/aeroelastic/{types.ts => index.d.ts} | 33 ++++++-- .../canvas/public/lib/aeroelastic/index.js | 2 +- .../canvas/public/lib/aeroelastic/layout.js | 2 +- .../canvas/public/lib/aeroelastic/matrix.ts | 2 +- .../canvas/public/lib/aeroelastic/matrix2d.ts | 2 +- .../lib/aeroelastic/{state.ts => select.ts} | 51 ++++-------- .../public/lib/aeroelastic/tsconfig.json | 21 +++++ x-pack/tsconfig.json | 3 +- yarn.lock | 14 ++-- 13 files changed, 166 insertions(+), 52 deletions(-) create mode 100644 x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts rename x-pack/plugins/canvas/public/lib/aeroelastic/{types.ts => index.d.ts} (53%) rename x-pack/plugins/canvas/public/lib/aeroelastic/{state.ts => select.ts} (62%) create mode 100644 x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json diff --git a/package.json b/package.json index f47dc627b86a8..011f39aab9afb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test:ui:runner": "node scripts/functional_test_runner", "test:server": "grunt test:server", "test:coverage": "grunt test:coverage", + "typespec": "typings-tester --config x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "grunt licenses --dev", "build": "node scripts/build --all-platforms", "start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ", @@ -410,6 +411,7 @@ "tslint-microsoft-contrib": "^6.0.0", "tslint-plugin-prettier": "^2.0.0", "typescript": "^3.3.3333", + "typings-tester": "^0.3.2", "vinyl-fs": "^3.0.2", "xml2js": "^0.4.19", "xmlbuilder": "9.0.4", diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts new file mode 100644 index 0000000000000..274e0e64ba361 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts @@ -0,0 +1,82 @@ +import { select } from '../../select'; +import { Json, Selector } from '../..'; + +/* + + Type checking isn't too useful if future commits can accidentally weaken the type constraints, because a + TypeScript linter will not complain - everything that passed before will continue to pass. The coder + will not have feedback that the original intent with the typing got compromised. To declare the intent + via passing and failing type checks, test cases are needed, some of which designed to expect a TS pass, + some of them to expect a TS complaint. It documents intent for peers too, as type specs are a tough read. + + Run compile-time type specification tests in the `kibana` root with: + + yarn typespec + + Test "cases" expecting to pass TS checks are not annotated, while ones we want TS to complain about + are prepended with the comment + + // typings:expect-error + + The test "suite" and "cases" are wrapped in IIFEs to prevent linters from complaining about the unused + binding. It can be structured internally as desired. + +*/ + +((): void => { + /** + * TYPE TEST SUITE + */ + + (function jsonTests(plain: Json): void { + // numbers are OK + plain = 1; + plain = NaN; + plain = Infinity; + plain = -Infinity; + plain = Math.pow(2, 6); + // other JSON primitive types are OK + plain = false; + plain = 'hello'; + plain = null; + // structures made of above and of structures are OK + plain = {}; + plain = []; + plain = { a: 1 }; + plain = [0, null, false, NaN, 3.14, 'one more']; + plain = { a: { b: 5, c: { d: [1, 'a', -Infinity, null], e: -1 }, f: 'b' }, g: false }; + + // typings:expect-error + plain = undefined; // it's undefined + // typings:expect-error + plain = a => a; // it's a function + // typings:expect-error + plain = [new Date()]; // it's a time + // typings:expect-error + plain = { a: Symbol('haha') }; // symbol isn't permitted either + // typings:expect-error + plain = window || void 0; + // typings:expect-error + plain = { a: { b: 5, c: { d: [1, 'a', undefined, null] } } }; // going deep into the structure + + return; // jsonTests + })(null); + + (function selectTests(selector: Selector): void { + selector = select((a: Json) => a); // one arg + selector = select((a: Json, b: Json): Json => `${a} and ${b}`); // more args + selector = select(() => 1); // zero arg + selector = select((...args: Json[]) => args); // variadic + + // typings:expect-error + selector = (a: Json) => a; // not a selector + // typings:expect-error + selector = select(() => {}); // should yield a JSON value, but it returns void + // typings:expect-error + selector = select((x: Json) => ({ a: x, b: undefined })); // should return a Json + + return; // selectTests + })(select((a: Json) => a)); + + return; // test suite +})(); diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/dom.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/dom.ts index 5bd7325fa6808..4d674d2719419 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/dom.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/dom.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { transformMatrix3d } from './types'; +import { transformMatrix3d } from '.'; // converts a transform matrix to a CSS string export const matrixToCSS = (transformMatrix: transformMatrix3d): string => diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js b/x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js index 5a4b0285885cb..a1552b2709e59 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { select, selectReduce } from './state'; +import { select, selectReduce } from './select'; // Only needed to shuffle some modifier keys for Apple keyboards as per vector editing software conventions, // so it's OK that user agent strings are not reliable; in case it's spoofed, it'll just work with a slightly diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/types.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts similarity index 53% rename from x-pack/plugins/canvas/public/lib/aeroelastic/types.ts rename to x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts index 2f58c9f12eeda..07c812b5bf648 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/types.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// linear algebra type f64 = number; // eventual AssemblyScript compatibility; doesn't hurt with vanilla TS either type f = f64; // shorthand @@ -15,16 +16,36 @@ export type transformMatrix2d = [f, f, f, f, f, f, f, f, f] & export type transformMatrix3d = [f, f, f, f, f, f, f, f, f, f, f, f, f, f, f, f] & ReadonlyArray & { __nominal: 'transformMatrix3d' }; +// plain, JSON-bijective value +export type Json = JsonPrimitive | JsonArray | JsonMap; +type JsonPrimitive = null | boolean | number | string; +interface JsonArray extends Array {} +interface JsonMap extends IMap {} +interface IMap { + [key: string]: T; +} + +// state object +export type State = JsonMap & WithActionId; +export type ActionId = number; +interface WithActionId { + primaryUpdate: { type: string; payload: { uid: ActionId; [propName: string]: Json } }; + [propName: string]: Json; // allow other arbitrary props +} + +// reselect-based data flow +export type PlainFun = (...args: Json[]) => Json; +export type Selector = (...fns: Resolve[]) => Resolve; +type Resolve = ((obj: State) => Json); + +// export interface Meta { silent: boolean; } -export type ActionId = number; export type TypeName = string; -export type NodeResult = any; -export type Payload = any; -export type NodeFunction = (...args: any[]) => any; -export type UpdaterFunction = (arg: NodeResult) => NodeResult; +export type Payload = JsonMap; +export type UpdaterFunction = (arg: State) => State; export type ChangeCallbackFunction = ( - { type, state }: { type: TypeName; state: NodeResult }, + { type, state }: { type: TypeName; state: State }, meta: Meta ) => void; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/index.js b/x-pack/plugins/canvas/public/lib/aeroelastic/index.js index 132f4dcd23d75..e0b74ccb6c8eb 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/index.js +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/index.js @@ -8,7 +8,7 @@ import { matrixToCSS } from './dom'; import { nextScene } from './layout'; import { primaryUpdate } from './layout_functions'; import { multiply, rotateZ, translate } from './matrix'; -import { createStore, select } from './state'; +import { createStore, select } from './select'; export const layout = { nextScene, primaryUpdate }; export const matrix = { multiply, rotateZ, translate }; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/layout.js b/x-pack/plugins/canvas/public/lib/aeroelastic/layout.js index 011d066def20d..35c16ff701bc1 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/layout.js +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/layout.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { select } from './state'; +import { select } from './select'; import { actionEvent, diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts index 175bd27eef504..ae281d5a7b4c6 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix.ts @@ -24,7 +24,7 @@ * */ -import { transformMatrix3d, vector3d } from './types'; +import { transformMatrix3d, vector3d } from '.'; const NANMATRIX = [ NaN, diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts index bd23c5c3c6d68..8faae5420b91c 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/matrix2d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { transformMatrix2d, vector2d } from './types'; +import { transformMatrix2d, vector2d } from '.'; export const ORIGIN = [0, 0, 1] as vector2d; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/state.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts similarity index 62% rename from x-pack/plugins/canvas/public/lib/aeroelastic/state.ts rename to x-pack/plugins/canvas/public/lib/aeroelastic/select.ts index d1d6bc87847e9..17b46f660b41c 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/state.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/select.ts @@ -7,13 +7,15 @@ import { ActionId, ChangeCallbackFunction, + Json, Meta, - NodeFunction, - NodeResult, Payload, + PlainFun, + Selector, + State, TypeName, UpdaterFunction, -} from './types'; +} from './index'; export const shallowEqual = (a: any, b: any): boolean => { if (a === b) { @@ -32,15 +34,20 @@ export const shallowEqual = (a: any, b: any): boolean => { const makeUid = (): ActionId => 1e11 + Math.floor((1e12 - 1e11) * Math.random()); -export const selectReduce = (fun: NodeFunction, previousValue: NodeResult): NodeFunction => ( - ...inputs: NodeFunction[] -): NodeResult => { +export const select = (fun: PlainFun): Selector => (...fns) => { + let { prevId, cache } = { prevId: NaN as ActionId, cache: null as Json }; + const old = (object: State): boolean => prevId === (prevId = object.primaryUpdate.payload.uid); + return obj => (old(obj) ? cache : (cache = fun(...fns.map(f => f(obj) as Json)))); +}; + +// this function `selectReduce` is in the process of being removed in another PR +export const selectReduce = (fun: PlainFun, previousValue: Json): Selector => (...inputs) => { // last-value memoizing version of this single line function: // (fun, previousValue) => (...inputs) => state => previousValue = fun(previousValue, ...inputs.map(input => input(state))) - let argumentValues = [] as NodeResult[]; + let argumentValues = [] as Json[]; let value = previousValue; let prevValue = previousValue; - return (state: NodeResult) => { + return (state: State) => { if ( shallowEqual(argumentValues, (argumentValues = inputs.map(input => input(state)))) && value === prevValue @@ -54,32 +61,10 @@ export const selectReduce = (fun: NodeFunction, previousValue: NodeResult): Node }; }; -export const select = (fun: NodeFunction): NodeFunction => ( - ...inputs: NodeFunction[] -): NodeResult => { - // last-value memoizing version of this single line function: - // fun => (...inputs) => state => fun(...inputs.map(input => input(state))) - let argumentValues = [] as NodeResult[]; - let value: NodeResult; - let actionId: ActionId; - return (state: NodeResult) => { - const lastActionId: ActionId = state.primaryUpdate.payload.uid; - if ( - actionId === lastActionId || - shallowEqual(argumentValues, (argumentValues = inputs.map(input => input(state)))) - ) { - return value; - } - - value = fun(...argumentValues); - actionId = lastActionId; - return value; - }; -}; - -export const createStore = (initialState: NodeResult, onChangeCallback: ChangeCallbackFunction) => { +// this function `createStore` is in the process of being removed in another PR +export const createStore = (initialState: State, onChangeCallback: ChangeCallbackFunction) => { let currentState = initialState; - let updater: UpdaterFunction = (state: NodeResult): NodeResult => state; // default: no side effect + let updater: UpdaterFunction = (state: State): State => state; // default: no side effect const getCurrentState = () => currentState; // const setCurrentState = newState => (currentState = newState); const setUpdater = (updaterFunction: UpdaterFunction) => { diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json new file mode 100644 index 0000000000000..94081bdbd55a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../../tsconfig", + "compilerOptions": { + "module": "commonjs", + "lib": ["esnext", "dom"], + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": false, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "layout/*": ["aeroelastic/*"] + }, + "types": ["@kbn/x-pack/plugins/canvas/public/lib/aeroelastic"] + }, + "exclude": ["node_modules", "**/*.spec.ts", "node_modules/@types/mocha"] +} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index c74d60e902326..dd3ab033718b8 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -8,7 +8,8 @@ "test_utils/**/*" ], "exclude": [ - "test/**/*" + "test/**/*", + "**/typespec_tests.ts" ], "compilerOptions": { "outDir": ".", diff --git a/yarn.lock b/yarn.lock index 3ed6a16f65841..1063702a9cc52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6687,7 +6687,7 @@ commander@0.6.1: resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= -commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.9.0: +commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.1, commander@^2.12.2, commander@^2.19.0, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -23580,12 +23580,14 @@ typescript@^3.3.3333: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3333.tgz#171b2c5af66c59e9431199117a3bcadc66fdcfd6" integrity sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw== -ua-parser-js@^0.7.18: - version "0.7.18" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" - integrity sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA== +typings-tester@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/typings-tester/-/typings-tester-0.3.2.tgz#04cc499d15ab1d8b2d14dd48415a13d01333bc5b" + integrity sha512-HjGoAM2UoGhmSKKy23TYEKkxlphdJFdix5VvqWFLzH1BJVnnwG38tpC6SXPgqhfFGfHY77RlN1K8ts0dbWBQ7A== + dependencies: + commander "^2.12.2" -ua-parser-js@^0.7.9: +ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: version "0.7.19" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==