[APM] Optimize idx calls to native optional chaining (#34841) (#35432)

* [APM] Add new kibana package '@kbn/babel-plugin-apm-idx' based on
'babel-plugin-idx' to work with the APM implementation of the deep
object property helper function. Configure it as a babel plugin in the
common babel preset.

* [APM] modified the expanded output code to handle null checking correction for nested properties and added tests.

* [APM] use same versions of babel and jest as core kibana. fixes broken tests.

* [APM] created namespaced package `@kbn/elastic-idx`. Made available the
apm/common/idx and the corresponding babel plugin under this package.
Modified all imports of apm/common/idx to @kbn/elastic-idx.

* [APM] add typescript build to the @kbn/elastic-idx package

* [APM] fix idx import and linting failures

* [APM] make @kbn/elastic-idx/babel plugin detect idx calls as member functions from the bound scope
This commit is contained in:
Oliver Gupte 2019-04-22 14:39:33 -07:00 committed by GitHub
parent 161c2f56c8
commit ab4be80d4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1224 additions and 42 deletions

View file

@ -133,6 +133,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
This product includes code that is based on facebookincubator/idx, which was
available under a "MIT" license.
MIT License
Copyright (c) 2013-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This product includes code that is based on flot-charts, which was available
under a "MIT" license.

View file

@ -18,11 +18,9 @@
*/
module.exports = {
presets: [
require.resolve('@babel/preset-typescript'),
require.resolve('@babel/preset-react')
],
presets: [require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
require.resolve('@kbn/elastic-idx/babel'),
require.resolve('babel-plugin-add-module-exports'),
// The class properties proposal was merged with the private fields proposal
@ -41,11 +39,7 @@ module.exports = {
//
// See https://github.com/babel/babel/issues/8244#issuecomment-466548733
test: /x-pack[\/\\]plugins[\/\\]infra[\/\\].*[\/\\]graphql/,
plugins: [
[
require.resolve('babel-plugin-typescript-strip-namespaces'),
],
]
}
]
plugins: [[require.resolve('babel-plugin-typescript-strip-namespaces')]],
},
],
};

View file

@ -8,6 +8,7 @@
"@babel/preset-react":"^7.0.0",
"@babel/preset-env": "^7.3.4",
"@babel/preset-typescript": "^7.3.3",
"@kbn/elastic-idx": "1.0.0",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-transform-define": "^1.3.1",
"babel-plugin-typescript-strip-namespaces": "^1.1.1"

View file

@ -0,0 +1,3 @@
/tsconfig.json
/src
/babel/index.test.js

View file

@ -0,0 +1,76 @@
Kibana elastic-idx Library
==========================
The `@kbn/elastic-idx` package provides the `idx` function used for optional
chaining. Currently, the optional chaining draft is in stage 1, making it too
uncertain to add syntax support within Kibana. Other optional chaining
libraries require the Proxy object to be polyfilled for browser support,
however, this polyfill is not fully supported across all browsers that Kibana
requires. The facebookincubator `idx` project
(https://github.com/facebookincubator/idx) provides an answer to this with a
specific implementation that is understood by TypeScript so that type
information does not get lost (unlike lodash get) The `@kbn/elastic-idx`
library makes use the `idx` idiom but differs in the way null values within the
property chain are handled.
Similar to the facebookincubator `idx` project, `@kbn/elastic-idx` also
provides the Babel plugin to transform `idx()` function calls into the expanded
form. This Babel plugin was based off the facebookincubator `idx` Babel
plugin, since the invocation syntax is almost identical, but the transformed
code differs to match how the `@kbn/elastic-idx` library treats null values.
App Usage
----------
Within Kibana, `@kbn/elastic-idx` can be imported and used in any JavaScript or
TypeScript project:
```
import { idx } from '@kbn/elastic-idx';
const obj0 = { a: { b: { c: { d: 'iamdefined' } } } };
const obj1 = { a: { b: null } };
idx(obj0, _ => _.a.b.c.d); // returns 'iamdefined'
idx(obj1, _ => _.a.b.c.e); // returns undefined
idx(obj1, _ => _.a.b); // returns null
```
Build Optimization
-------------------
Similar to the facebookincubator `idx` project, it is NOT RECOMMENDED to use
idx in shipped app code. The implementation details which make
`@kbn/elastic-idx` possible comes at a non-negligible performance cost. This
usually isn't noticable during development, but for production builds, it is
recommended to transform idx calls into native, expanded form JS. Use the
plugin `@kbn/elastic-idx/babel` within your Babel configuration:
```
{ "plugins": [ "@kbn/elastic-idx/babel" ] }
```
The resulting Babel transforms the following:
```
import { idx } from '@kbn/elastic-idx';
const obj = { a: { b: { c: { d: 'iamdefined' } } } };
idx(obj, _ => _.a.b.c.d);
```
into this:
```
obj != null &&
obj.a != null &&
obj.a.b != null &&
obj.a.b.c != null ?
obj.a.b.c.d : undefined
```
Note that this also removes the import statement from the source code, since it
no longer needs to be bundled.
Testing
--------
Tests can be run with `npm test`. This includes "functional" tests that
transform and evaluate idx calls.

View file

@ -0,0 +1,321 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* @notice
* This product includes code that is based on facebookincubator/idx, which was
* available under a "MIT" license.
*
* MIT License
*
* Copyright (c) 2013-present, Facebook, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* eslint strict: 0, new-cap: 0 */
'use strict';
module.exports = context => {
const t = context.types;
const idxRe = /\bidx\b/;
function checkIdxArguments(file, node) {
const args = node.arguments;
if (args.length !== 2) {
throw file.buildCodeFrameError(node, 'The `idx` function takes exactly two arguments.');
}
const arrowFunction = args[1];
if (!t.isArrowFunctionExpression(arrowFunction)) {
throw file.buildCodeFrameError(
arrowFunction,
'The second argument supplied to `idx` must be an arrow function.'
);
}
if (!t.isExpression(arrowFunction.body)) {
throw file.buildCodeFrameError(
arrowFunction.body,
'The body of the arrow function supplied to `idx` must be a single ' +
'expression (without curly braces).'
);
}
if (arrowFunction.params.length !== 1) {
throw file.buildCodeFrameError(
arrowFunction.params[2] || arrowFunction,
'The arrow function supplied to `idx` must take exactly one parameter.'
);
}
const input = arrowFunction.params[0];
if (!t.isIdentifier(input)) {
throw file.buildCodeFrameError(
arrowFunction.params[0],
'The parameter supplied to `idx` must be an identifier.'
);
}
}
function checkIdxBindingNode(file, node) {
if (t.isImportDeclaration(node)) {
// E.g. `import '...'`
if (node.specifiers.length === 0) {
throw file.buildCodeFrameError(node, 'The idx import must have a value.');
}
// E.g. `import A, {B} from '...'`
// `import A, * as B from '...'`
// `import {A, B} from '...'`
if (node.specifiers.length > 1) {
throw file.buildCodeFrameError(
node.specifiers[1],
'The idx import must be a single specifier.'
);
}
// `importKind` is not a property unless flow syntax is enabled.
// On specifiers, `importKind` is not "value" when it's not a type, it's
// `null`.
// E.g. `import type {...} from '...'`
// `import typeof {...} from '...'`
// `import {type ...} from '...'`.
// `import {typeof ...} from '...'`
if (
node.importKind === 'type' ||
node.importKind === 'typeof' ||
node.specifiers[0].importKind === 'type' ||
node.specifiers[0].importKind === 'typeof'
) {
throw file.buildCodeFrameError(node, 'The idx import must be a value import.');
}
} else if (t.isVariableDeclarator(node)) {
// E.g. var {idx} or var [idx]
if (!t.isIdentifier(node.id)) {
throw file.buildCodeFrameError(
node.specifiers[0],
'The idx declaration must be an identifier.'
);
}
}
}
class UnsupportedNodeTypeError extends Error {
constructor(node, ...params) {
super(`Node type is not supported: ${node.type}`, ...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, UnsupportedNodeTypeError);
}
this.name = 'UnsupportedNodeTypeError';
}
}
function getDeepProperties(node, properties = [], computedProperties = new Set()) {
if (t.isMemberExpression(node)) {
if (node.computed) {
computedProperties.add(node.property);
}
return getDeepProperties(node.object, [node.property, ...properties], computedProperties);
} else if (t.isIdentifier(node)) {
return [[node, ...properties], computedProperties];
}
throw new UnsupportedNodeTypeError(node);
}
function buildMemberChain(properties, computedProperties) {
if (properties.length > 1) {
const lead = properties.slice(0, properties.length - 1);
const last = properties[properties.length - 1];
return t.MemberExpression(
buildMemberChain(lead, computedProperties),
last,
computedProperties.has(last)
);
} else if (properties.length === 1) {
return properties[0];
}
return t.identifier('undefined');
}
function buildExpandedMemberNullChecks(
leadingProperties = [],
trailingProperties = [],
computedProperties
) {
const propertyChainNullCheck = t.BinaryExpression(
'!=',
buildMemberChain(leadingProperties, computedProperties),
t.NullLiteral()
);
if (trailingProperties.length <= 1) {
return propertyChainNullCheck;
}
const [headTrailingProperty, ...tailProperties] = trailingProperties;
return t.LogicalExpression(
'&&',
propertyChainNullCheck,
buildExpandedMemberNullChecks(
[...leadingProperties, headTrailingProperty],
tailProperties,
computedProperties
)
);
}
function buildExpandedMemberAccess(node, state) {
let baseNode;
let properties;
let computedProperties;
try {
[[baseNode, ...properties], computedProperties] = getDeepProperties(node);
} catch (error) {
if (error instanceof UnsupportedNodeTypeError) {
throw state.file.buildCodeFrameError(
node,
'idx callbacks may only access properties on the callback parameter.'
);
}
throw error;
}
if (baseNode.name !== state.base.name) {
throw state.file.buildCodeFrameError(
node,
'The parameter of the arrow function supplied to `idx` must match ' +
'the base of the body expression.'
);
}
return t.ConditionalExpression(
buildExpandedMemberNullChecks([state.input], properties, computedProperties),
buildMemberChain([state.input, ...properties], computedProperties),
t.identifier('undefined')
);
}
function visitIdxCallExpression(path, state) {
const node = path.node;
checkIdxArguments(state.file, node);
const replacement = buildExpandedMemberAccess(node.arguments[1].body, {
file: state.file,
input: node.arguments[0],
base: node.arguments[1].params[0],
});
path.replaceWith(replacement);
}
function isIdxImportOrRequire(node, name) {
if (t.isImportDeclaration(node)) {
if (name instanceof RegExp) {
return name.test(node.source.value);
} else {
return t.isStringLiteral(node.source, { value: name });
}
} else if (t.isVariableDeclarator(node)) {
return (
t.isCallExpression(node.init) &&
t.isIdentifier(node.init.callee, { name: 'require' }) &&
(name instanceof RegExp
? name.test(node.init.arguments[0].value)
: t.isLiteral(node.init.arguments[0], { value: name }))
);
} else {
return false;
}
}
const declareVisitor = {
'ImportDeclaration|VariableDeclarator'(path, state) {
if (!isIdxImportOrRequire(path.node, state.importName)) {
return;
}
checkIdxBindingNode(state.file, path.node);
const bindingName = t.isImportDeclaration(path.node)
? path.node.specifiers[0].local.name
: path.node.id.name;
const idxBinding = path.scope.getOwnBinding(bindingName);
idxBinding.constantViolations.forEach(refPath => {
throw state.file.buildCodeFrameError(refPath.node, '`idx` cannot be redefined.');
});
let didTransform = false;
let didSkip = false;
// Traverse the references backwards to process inner calls before
// outer calls.
idxBinding.referencePaths
.slice()
.reverse()
.forEach(refPath => {
if (refPath.node === idxBinding.node) {
// Do nothing...
} else if (refPath.parentPath.isMemberExpression()) {
visitIdxCallExpression(refPath.parentPath.parentPath, state);
didTransform = true;
} else if (refPath.parentPath.isCallExpression()) {
visitIdxCallExpression(refPath.parentPath, state);
didTransform = true;
} else {
// Should this throw?
didSkip = true;
}
});
if (didTransform && !didSkip) {
path.remove();
}
},
};
return {
visitor: {
Program(path, state) {
const importName = state.opts.importName || '@kbn/elastic-idx';
// If there can't reasonably be an idx call, exit fast.
if (importName !== '@kbn/elastic-idx' || idxRe.test(state.file.code)) {
// We're very strict about the shape of idx. Some transforms, like
// "babel-plugin-transform-async-to-generator", will convert arrow
// functions inside async functions into regular functions. So we do
// our transformation before any one else interferes.
const newState = { file: state.file, importName };
path.traverse(declareVisitor, newState);
}
},
},
};
};

View file

@ -0,0 +1,711 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* @notice
* This product includes code that is based on facebookincubator/idx, which was
* available under a "MIT" license.
*
* MIT License
*
* Copyright (c) 2013-present, Facebook, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
'use strict'; // eslint-disable-line strict
jest.autoMockOff();
const { transformSync: babelTransform } = require('@babel/core');
const babelPluginIdx = require('./index');
const transformAsyncToGenerator = require('@babel/plugin-transform-async-to-generator');
const vm = require('vm');
function transform(source, plugins, options) {
return babelTransform(source, {
plugins: plugins || [[babelPluginIdx, options]],
babelrc: false,
highlightCode: false,
}).code;
}
const asyncToGeneratorHelperCode = `
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
`;
function stringByTrimmingSpaces(string) {
return string.replace(/\s+/g, '');
}
expect.extend({
toTransformInto: (input, expected) => {
const plugins = typeof input === 'string' ? null : input.plugins;
const options = typeof input === 'string' ? undefined : input.options;
const code = typeof input === 'string' ? input : input.code;
const actual = transform(code, plugins, options);
const pass = stringByTrimmingSpaces(actual) === stringByTrimmingSpaces(expected);
return {
pass,
message: () =>
'Expected input to transform into:\n' + expected + '\n' + 'Instead, got:\n' + actual,
};
},
toThrowTransformError: (input, expected) => {
try {
const plugins = typeof input === 'string' ? null : input.plugins;
const options = typeof input === 'string' ? undefined : input.options;
const code = typeof input === 'string' ? input : input.code;
transform(code, plugins, options);
} catch (error) {
const actual = /^.+:\s*(.*)/.exec(error.message)[1]; // Strip "undefined: " and code snippet
return {
pass: actual === expected,
message: () =>
'Expected transform to throw "' + expected + '", but instead ' + 'got "' + actual + '".',
};
}
return {
pass: false,
message: () => 'Expected transform to throw "' + expected + '".',
};
},
toReturn: (input, expected) => {
const code = transform(input, undefined);
const actual = vm.runInNewContext(code);
return {
pass: actual === expected,
message: () => 'Expected "' + expected + '" but got "' + actual + '".',
};
},
});
describe('kbn-babel-plugin-apm-idx', () => {
it('transforms member expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b.c.d.e);
`).toTransformInto(`
base != null && base.b != null && base.b.c != null && base.b.c.d != null
? base.b.c.d.e
: undefined;
`);
});
it('throws on call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b.c(...foo)().d(bar, null, [...baz]));
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms bracket notation', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _["b"][0][c + d]);
`).toTransformInto(`
base != null && base["b"] != null && base["b"][0] != null ? base["b"][0][c + d] : undefined;
`);
});
it('throws on bracket notation call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _["b"](...foo)()[0][c + d](bar, null, [...baz]));
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms combination of both member access notations', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.a["b"].c[d[e[f]]].g);
`).toTransformInto(`
base != null && base.a != null && base.a["b"] != null && base.a["b"].c != null && base.a["b"].c[d[e[f]]] != null
? base.a["b"].c[d[e[f]]].g
: undefined;
`);
});
it('transforms if the base is an expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(this.props.base[5], _ => _.property);
`).toTransformInto(`
this.props.base[5] != null ? this.props.base[5].property : undefined;
`);
});
it('throws if the arrow function has more than one param', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, (a, b) => _.property);
`).toThrowTransformError(
'The arrow function supplied to `idx` must take exactly one parameter.'
);
});
it('throws if the arrow function has an invalid base', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, a => b.property)
`).toThrowTransformError(
'The parameter of the arrow function supplied to `idx` must match the ' +
'base of the body expression.'
);
});
it('throws if the arrow function expression has non-properties/methods', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => (_.a++).b.c);
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('throws if the body of the arrow function is not an expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => {})
`).toThrowTransformError(
'The body of the arrow function supplied to `idx` must be a single ' +
'expression (without curly braces).'
);
});
it('ignores non-function call idx', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
result = idx;
`).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
result = idx;
`);
});
it('throws if idx is called with zero arguments', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx();
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('throws if idx is called with one argument', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(1);
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('throws if idx is called with three arguments', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(1, 2, 3);
`).toThrowTransformError('The `idx` function takes exactly two arguments.');
});
it('transforms idx calls as part of another expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
paddingStatement();
a = idx(base, _ => _.b[c]);
`).toTransformInto(`
paddingStatement();
a = base != null && base.b != null ? base.b[c] : undefined;
`);
});
it('transforms nested idx calls', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toTransformInto(`
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
) != null
&&
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
).e != null ?
(
(base != null && base.a != null ? base.a.b : undefined) != null &&
(base != null && base.a != null ? base.a.b : undefined).c != null ?
(base != null && base.a != null ? base.a.b : undefined).c.d :
undefined
).e.f :
undefined;
`);
});
it('transforms idx calls inside async functions (plugin order #1)', () => {
expect({
plugins: [babelPluginIdx, transformAsyncToGenerator],
code: `
import { idx } from '@kbn/elastic-idx';
async function f() {
idx(base, _ => _.b.c.d.e);
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
function f() {
return _f.apply(this, arguments);
}
function _f() {
_f = _asyncToGenerator(function* () {
base != null && base.b != null && base.b.c != null && base.b.c.d != null ? base.b.c.d.e : undefined;
});
return _f.apply(this, arguments);
}
`);
});
it('transforms idx calls inside async functions (plugin order #2)', () => {
expect({
plugins: [transformAsyncToGenerator, babelPluginIdx],
code: `
import { idx } from '@kbn/elastic-idx';
async function f() {
idx(base, _ => _.b.c.d.e);
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
function f() {
return _f.apply(this, arguments);
}
function _f() {
_f = _asyncToGenerator(function* () {
base != null && base.b != null && base.b.c != null && base.b.c.d != null ? base.b.c.d.e : undefined;
});
return _f.apply(this, arguments);
}
`);
});
it('transforms idx calls in async methods', () => {
expect({
plugins: [transformAsyncToGenerator, babelPluginIdx],
code: `
import { idx } from '@kbn/elastic-idx';
class Foo {
async bar() {
idx(base, _ => _.b);
return this;
}
}
`,
}).toTransformInto(`
${asyncToGeneratorHelperCode}
class Foo {
bar() {
var _this = this;
return _asyncToGenerator(function* () {
base != null ? base.b : undefined;
return _this;
})();
}
}
`);
});
it('transforms idx calls when an idx import binding is in scope', () => {
expect(`
import idx from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('transforms idx calls when an idx const binding is in scope', () => {
expect(`
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('transforms deep idx calls when an idx import binding is in scope', () => {
expect(`
import idx from '@kbn/elastic-idx';
function f() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function f() {
base != null ? base.b : undefined;
}
`);
});
it('transforms deep idx calls when an idx const binding is in scope', () => {
expect(`
const idx = require('@kbn/elastic-idx');
function f() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function f() {
base != null ? base.b : undefined;
}
`);
});
it('transforms idx calls when an idx is called as a member function on the binding in scope', () => {
expect(`
const elastic_idx = require("@kbn/elastic-idx");
const result = elastic_idx.idx(base, _ => _.a.b.c.d);
`).toTransformInto(`
const result = base != null &&
base.a != null &&
base.a.b != null &&
base.a.b.c != null ?
base.a.b.c.d :
undefined;
`);
});
it('throws on base call expressions', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _().b.c);
`).toThrowTransformError('idx callbacks may only access properties on the callback parameter.');
});
it('transforms when the idx parent is a scope creating expression', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
(() => idx(base, _ => _.b));
`).toTransformInto(`
() => base != null ? base.b : undefined;
`);
});
it('throws if redefined before use', () => {
expect(`
let idx = require('@kbn/elastic-idx');
idx = null;
idx(base, _ => _.b);
`).toThrowTransformError('`idx` cannot be redefined.');
});
it('throws if redefined after use', () => {
expect(`
let idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
idx = null;
`).toThrowTransformError('`idx` cannot be redefined.');
});
it('throws if there is a duplicate declaration', () => {
expect(() =>
transform(`
let idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
function idx() {}
`)
).toThrow();
});
it('handles sibling scopes with unique idx', () => {
expect(`
function aaa() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
function bbb() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
`).toTransformInto(`
function aaa() {
base != null ? base.b : undefined;
}
function bbb() {
base != null ? base.b : undefined;
}
`);
});
it('handles sibling scopes with and without idx', () => {
expect(`
function aaa() {
const idx = require('@kbn/elastic-idx');
idx(base, _ => _.b);
}
function bbb() {
idx(base, _ => _.b);
}
`).toTransformInto(`
function aaa() {
base != null ? base.b : undefined;
}
function bbb() {
idx(base, _ => _.b);
}
`);
});
it('handles nested scopes with shadowing', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b);
function aaa() {
idx(base, _ => _.b);
function bbb(idx) {
idx(base, _ => _.b);
}
}
`).toTransformInto(`
base != null ? base.b : undefined;
function aaa() {
base != null ? base.b : undefined;
function bbb(idx) {
idx(base, _ => _.b);
}
}
`);
});
it('handles named idx import', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('throws on default plus named import', () => {
expect(`
import idx, {foo} from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('throws on default plus namespace import', () => {
expect(`
import idx, * as foo from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('throws on named default plus other import', () => {
expect(`
import {default as idx, foo} from '@kbn/elastic-idx';
idx(base, _ => _.b);
`).toThrowTransformError('The idx import must be a single specifier.');
});
it('unused idx import should be left alone', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
`).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
`);
});
it('allows configuration of the import name', () => {
expect({
code: `
import { idx } from 'i_d_x';
idx(base, _ => _.b);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('follows configuration of the import name', () => {
expect({
code: `
import { idx } from '@kbn/elastic-idx';
import { idx as i_d_x } from 'i_d_x';
i_d_x(base, _ => _.b);
idx(base, _ => _.c);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
import { idx } from '@kbn/elastic-idx';
base != null ? base.b : undefined;
idx(base, _ => _.c);
`);
});
it('allows configuration of the require name as a string', () => {
expect({
code: `
import { idx } from 'i_d_x';
idx(base, _ => _.b);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('allows configuration of the require name as a RegExp', () => {
expect({
code: `
import { idx } from '../../common/idx';
idx(base, _ => _.b);
`,
options: { importName: /.*idx$/ },
}).toTransformInto(`
base != null ? base.b : undefined;
`);
});
it('follows configuration of the require name', () => {
expect({
code: `
const idx = require('@kbn/elastic-idx');
const i_d_x = require('i_d_x');
i_d_x(base, _ => _.b);
idx(base, _ => _.c);
`,
options: { importName: 'i_d_x' },
}).toTransformInto(`
const idx = require('@kbn/elastic-idx');
base != null ? base.b : undefined;
idx(base, _ => _.c);
`);
});
describe('functional', () => {
it('works with only properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: 2}}};
idx(base, _ => _.a.b.c);
`).toReturn(2);
});
it('works with missing properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {}}};
idx(base, _ => _.a.b.c);
`).toReturn(undefined);
});
it('works with null properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: null}};
idx(base, _ => _.a.b.c);
`).toReturn(undefined);
});
it('works with nested idx calls', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: {d: {e: {f: 2}}}}}};
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toReturn(2);
});
it('works with nested idx calls with missing properties', () => {
expect(`
import { idx } from '@kbn/elastic-idx';
const base = {a: {b: {c: null}}};
idx(
idx(
idx(base, _ => _.a.b),
_ => _.c.d
),
_ => _.e.f
);
`).toReturn(undefined);
});
});
});

View file

@ -0,0 +1,28 @@
{
"name": "@kbn/elastic-idx",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"description": "Library for optional chaining & the Babel plugin to transpile idx calls to plain, optimized JS",
"main": "target/index.js",
"types": "target/index.d.js",
"repository": {
"type": "git",
"url": "https://github.com/elastic/kibana/tree/master/packages/kbn-elastic-idx"
},
"scripts": {
"build": "tsc",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch",
"test": "jest"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-transform-async-to-generator": "^7.4.0",
"jest": "^24.1.0",
"typescript": "^3.3.3333"
},
"jest": {
"testEnvironment": "node"
}
}

View file

@ -1,7 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
@ -20,9 +33,9 @@ type DeepRequiredObject<T> = { [P in keyof T]-?: DeepRequired<T[P]> };
/**
* Function that has deeply required return type
*/
type FunctionWithRequiredReturnType<
T extends (...args: any[]) => any
> = T extends (...args: infer A) => infer R
type FunctionWithRequiredReturnType<T extends (...args: any[]) => any> = T extends (
...args: infer A
) => infer R
? (...args: A) => DeepRequired<R>
: never;

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "./target"
},
"include": ["src/**/*"]
}

View file

@ -154,6 +154,7 @@
"@elastic/node-crypto": "0.1.2",
"@elastic/numeral": "2.3.3",
"@kbn/babel-preset": "1.0.0",
"@kbn/elastic-idx": "1.0.0",
"@kbn/es-query": "1.0.0",
"@kbn/i18n": "1.0.0",
"@kbn/interpreter": "1.0.0",

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
export interface ErrorTab {

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import { idx } from '@kbn/elastic-idx';
import {
ERROR_EXC_HANDLED,
HTTP_REQUEST_METHOD,
@ -14,7 +15,6 @@ import {
USER_ID
} from '../../../../../common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { idx } from '../../../../../common/idx';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { APMLink } from '../../../shared/Links/APMLink';

View file

@ -17,7 +17,7 @@ import { Location } from 'history';
import React from 'react';
import styled from 'styled-components';
import { first } from 'lodash';
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
import { IUrlParams } from '../../../../store/urlParams';

View file

@ -18,8 +18,8 @@ import { i18n } from '@kbn/i18n';
import { Location } from 'history';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '@kbn/elastic-idx';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { idx } from '../../../../common/idx';
import { useFetcher } from '../../../hooks/useFetcher';
import {
loadErrorDistribution,

View file

@ -9,7 +9,7 @@ import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { fontSize } from '../../../../style/variables';
import { APMLink } from '../../../shared/Links/APMLink';

View file

@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { idx } from '@kbn/elastic-idx';
import {
TRANSACTION_DURATION,
TRANSACTION_RESULT,
@ -13,7 +14,6 @@ import {
USER_ID
} from '../../../../../common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { idx } from '../../../../../common/idx';
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
import { asPercent, asTime } from '../../../../utils/formatters';
import {

View file

@ -9,6 +9,7 @@ import styled from 'styled-components';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { idx } from '@kbn/elastic-idx';
import {
borderRadius,
fontFamilyCode,
@ -17,7 +18,6 @@ import {
unit,
units
} from '../../../../../../../style/variables';
import { idx } from '../../../../../../../../common/idx';
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
const ContextUrl = styled.div`

View file

@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n';
import { get, keys } from 'lodash';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '../../../../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink';

View file

@ -7,7 +7,7 @@
import { EuiCallOut, EuiHorizontalRule } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { idx } from '../../../../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction';
import { ElasticDocsLink } from '../../../../../../shared/Links/ElasticDocsLink';

View file

@ -15,7 +15,7 @@ import {
uniq,
zipObject
} from 'lodash';
import { idx } from '../../../../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace';
import { StringMap } from '../../../../../../../../typings/common';
import { Span } from '../../../../../../../../typings/es_schemas/ui/Span';

View file

@ -7,7 +7,7 @@
import { EuiBasicTable } from '@elastic/eui';
import { sortByOrder } from 'lodash';
import React, { Component } from 'react';
import { idx } from '../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
// TODO: this should really be imported from EUI
export interface ITableColumn<T> {

View file

@ -22,7 +22,7 @@ import { registerLanguage } from 'react-syntax-highlighter/dist/light';
// @ts-ignore
import { xcode } from 'react-syntax-highlighter/dist/styles';
import styled from 'styled-components';
import { idx } from '../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { IStackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/Stackframe';
import { borderRadius, px, unit, units } from '../../../style/variables';

View file

@ -7,7 +7,7 @@
import theme from '@elastic/eui/dist/eui_theme_light.json';
import React, { Fragment } from 'react';
import styled from 'styled-components';
import { idx } from '../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { IStackframe } from '../../../../typings/es_schemas/raw/fields/Stackframe';
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';

View file

@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { idx } from '../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransactionLink';
import { InfraLink } from '../Links/InfraLink';

View file

@ -5,13 +5,13 @@
*/
import { ESFilter } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
ERROR_GROUP_ID,
PROCESSOR_EVENT,
SERVICE_NAME,
TRANSACTION_SAMPLED
} from '../../../common/elasticsearch_fieldnames';
import { idx } from '../../../common/idx';
import { PromiseReturnType } from '../../../typings/common';
import { APMError } from '../../../typings/es_schemas/ui/APMError';
import { rangeFilter } from '../helpers/range_filter';

View file

@ -5,6 +5,7 @@
*/
import { SearchParams } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
ERROR_CULPRIT,
ERROR_EXC_HANDLED,
@ -14,7 +15,6 @@ import {
PROCESSOR_EVENT,
SERVICE_NAME
} from '../../../common/elasticsearch_fieldnames';
import { idx } from '../../../common/idx';
import { PromiseReturnType } from '../../../typings/common';
import { APMError } from '../../../typings/es_schemas/ui/APMError';
import { rangeFilter } from '../helpers/range_filter';

View file

@ -5,13 +5,13 @@
*/
import { BucketAgg, ESFilter } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
SERVICE_AGENT_NAME,
SERVICE_NAME,
TRANSACTION_TYPE
} from '../../../common/elasticsearch_fieldnames';
import { idx } from '../../../common/idx';
import { PromiseReturnType } from '../../../typings/common';
import { rangeFilter } from '../helpers/range_filter';
import { Setup } from '../helpers/setup_request';

View file

@ -5,13 +5,13 @@
*/
import { BucketAgg, ESFilter } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
SERVICE_AGENT_NAME,
SERVICE_NAME,
TRANSACTION_DURATION
} from '../../../../common/elasticsearch_fieldnames';
import { idx } from '../../../../common/idx';
import { PromiseReturnType } from '../../../../typings/common';
import { rangeFilter } from '../../helpers/range_filter';
import { Setup } from '../../helpers/setup_request';

View file

@ -5,7 +5,7 @@
*/
import moment from 'moment';
import { idx } from '../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { ESResponse } from './fetcher';
function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { getMlIndex } from '../../../../../common/ml_job_constants';
import { Setup } from '../../../helpers/setup_request';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { ESBucket, ESResponse } from './fetcher';
import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse';
import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform';

View file

@ -5,7 +5,7 @@
*/
import { first, last } from 'lodash';
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries';
import { ESBucket, ESResponse } from './fetcher';

View file

@ -5,8 +5,8 @@
*/
import { isNumber, round, sortBy } from 'lodash';
import { idx } from '@kbn/elastic-idx';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { idx } from '../../../../../common/idx';
import { Coordinate } from '../../../../../typings/timeseries';
import { ESResponse } from './fetcher';

View file

@ -5,7 +5,7 @@
*/
import { isEmpty } from 'lodash';
import { idx } from '../../../../../common/idx';
import { idx } from '@kbn/elastic-idx';
import { ESResponse } from './fetcher';
function getDefaultSample(buckets: IBucket[]) {

View file

@ -5,12 +5,12 @@
*/
import { ESFilter } from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import {
PROCESSOR_EVENT,
TRACE_ID,
TRANSACTION_ID
} from '../../../../common/elasticsearch_fieldnames';
import { idx } from '../../../../common/idx';
import { PromiseReturnType } from '../../../../typings/common';
import { Transaction } from '../../../../typings/es_schemas/ui/Transaction';
import { rangeFilter } from '../../helpers/range_filter';