Update painless autocomplete definitions (#85464) (#85593)

This commit is contained in:
Alison Goryachev 2020-12-10 13:03:57 -05:00 committed by GitHub
parent 088f301598
commit 99f426c347
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 5506 additions and 387670 deletions

View file

@ -44,7 +44,7 @@ module.exports = {
'filter',
'ip_script_field_script_field',
'long_script_field_script_field',
'painless_test',
'common',
'processor_conditional',
'score',
'string_script_field_script_field',

View file

@ -69,8 +69,8 @@ function start(opts) {
// Generate autocomplete definitions
painlessContextFolderContents
.filter((file) => {
// Expected filename format: whitelist-<contextName>.json
const contextName = file.split('.')[0].split('whitelist-').pop();
// Expected filename format: painless-<contextName>.json
const contextName = file.split('.')[0].split('painless-').pop();
return supportedContexts.includes(contextName);
})
.forEach((file) => {
@ -78,7 +78,8 @@ function start(opts) {
const { name, classes: painlessClasses } = JSON.parse(
readFileSync(join(esPainlessContextFolder, file), 'utf8')
);
const filePath = join(autocompleteOutputFolder, `${name}.json`);
const contextName = name ? name : 'common'; // The common allowlist does not have a name associated to it.
const filePath = join(autocompleteOutputFolder, `${contextName}.json`);
const code = JSON.stringify(
{ suggestions: createAutocompleteDefinitions(painlessClasses) },
null,

View file

@ -33,7 +33,8 @@ const esPainlessContextFolder = join(
'lang-painless',
'src',
'main',
'generated'
'generated',
'whitelist-json'
);
/**

View file

@ -49,38 +49,6 @@ const getDisplayName = (name, imported) => {
return displayName.replace('$', '.');
};
/**
* Filters the context data by primitives and returns an array of primitive names
* The current data structure from ES does not indicate if a field is
* a primitive or class, so we infer this by checking
* that no methods or fields are defined
* @param {string} contextData
* @returns {Array<String>}
*/
const getPrimitives = (contextData) => {
return contextData
.filter(
({
static_fields: staticFields,
fields,
static_methods: staticMethods,
methods,
constructors,
}) => {
if (
staticMethods.length === 0 &&
methods.length === 0 &&
staticFields.length === 0 &&
fields.length === 0 &&
constructors.length === 0
) {
return true;
}
}
)
.map((type) => type.name);
};
/**
* Given the method name, array of parameters, and return value,
* we create a description of the method that will be
@ -286,7 +254,6 @@ const createAutocompleteDefinitions = (painlessClasses) => {
}) => {
// The name is often prefixed by the Java package (e.g., Java.lang.Math) and needs to be removed
const displayName = getDisplayName(name, imported);
const isType = getPrimitives(painlessClasses).includes(name);
const properties = getPainlessClassToAutocomplete({
staticFields,
@ -299,8 +266,8 @@ const createAutocompleteDefinitions = (painlessClasses) => {
return {
label: displayName,
kind: isType ? 'type' : 'class',
documentation: isType ? `Primitive: ${displayName}` : `Class: ${displayName}`,
kind: 'class',
documentation: `Class: ${displayName}`,
insertText: displayName,
properties: properties.length ? properties : undefined,
constructorDefinition,
@ -313,7 +280,6 @@ const createAutocompleteDefinitions = (painlessClasses) => {
module.exports = {
getMethodDescription,
getPrimitives,
getPainlessClassToAutocomplete,
createAutocompleteDefinitions,
};

View file

@ -17,32 +17,13 @@
* under the License.
*/
const {
getPrimitives,
getMethodDescription,
getPainlessClassToAutocomplete,
createAutocompleteDefinitions,
} = require('./create_autocomplete_definitions');
// Snippet of sample data returned from GET _scripts/painless/_context?context=<context>
// Snippet of sample data returned from https://github.com/elastic/elasticsearch/tree/master/modules/lang-painless/src/main/generated/whitelist-json
const testContext = [
{
name: 'boolean',
imported: true,
constructors: [],
static_methods: [],
methods: [],
static_fields: [],
fields: [],
},
{
name: 'int',
imported: true,
constructors: [],
static_methods: [],
methods: [],
static_fields: [],
fields: [],
},
{
name: 'java.lang.Long',
imported: true,
@ -103,12 +84,6 @@ const testContext = [
];
describe('Autocomplete utils', () => {
describe('getPrimitives()', () => {
test('returns an array of primitives', () => {
expect(getPrimitives(testContext)).toEqual(['boolean', 'int']);
});
});
describe('getMethodDescription()', () => {
test('returns a string describing the method', () => {
expect(getMethodDescription('pow', [['double', 'double']], ['double'])).toEqual(
@ -128,7 +103,7 @@ describe('Autocomplete utils', () => {
describe('getPainlessClassToAutocomplete()', () => {
test('returns the fields and methods associated with a class', () => {
const mathClass = testContext[3];
const mathClass = testContext[1];
const {
static_fields: staticFields,
@ -173,7 +148,7 @@ describe('Autocomplete utils', () => {
});
test('removes duplicate methods', () => {
const longClass = testContext[2];
const longClass = testContext[0];
const {
static_fields: staticFields,
@ -251,22 +226,6 @@ describe('Autocomplete utils', () => {
describe('createAutocompleteDefinitions()', () => {
test('returns formatted autocomplete definitions', () => {
expect(createAutocompleteDefinitions(testContext)).toEqual([
{
properties: undefined,
constructorDefinition: undefined,
documentation: 'Primitive: boolean',
insertText: 'boolean',
kind: 'type',
label: 'boolean',
},
{
properties: undefined,
constructorDefinition: undefined,
documentation: 'Primitive: int',
insertText: 'int',
kind: 'type',
label: 'int',
},
{
constructorDefinition: undefined,
documentation: 'Class: Long',

View file

@ -20,7 +20,7 @@
import * as stringScriptFieldScriptFieldContext from './string_script_field_script_field.json';
import * as scoreContext from './score.json';
import * as processorConditionalContext from './processor_conditional.json';
import * as painlessTestContext from './painless_test.json';
import * as commonContext from './common.json';
import * as longScriptFieldScriptFieldContext from './long_script_field_script_field.json';
import * as ipScriptFieldScriptFieldContext from './ip_script_field_script_field.json';
import * as filterContext from './filter.json';
@ -30,7 +30,7 @@ import * as booleanScriptFieldScriptFieldContext from './boolean_script_field_sc
export { stringScriptFieldScriptFieldContext };
export { scoreContext };
export { processorConditionalContext };
export { painlessTestContext };
export { commonContext };
export { longScriptFieldScriptFieldContext };
export { ipScriptFieldScriptFieldContext };
export { filterContext };

View file

@ -23,31 +23,15 @@ import {
getStaticSuggestions,
getFieldSuggestions,
getClassMemberSuggestions,
getPrimitives,
getConstructorSuggestions,
getKeywords,
getTypeSuggestions,
Suggestion,
} from './autocomplete';
const keywords: PainlessCompletionItem[] = getKeywords();
const testSuggestions: Suggestion[] = [
{
properties: undefined,
constructorDefinition: undefined,
documentation: 'Primitive: boolean',
insertText: 'boolean',
kind: 'type',
label: 'boolean',
},
{
properties: undefined,
constructorDefinition: undefined,
documentation: 'Primitive: int',
insertText: 'int',
kind: 'type',
label: 'int',
},
{
properties: [
{
@ -97,18 +81,6 @@ describe('Autocomplete lib', () => {
expect(getStaticSuggestions({ suggestions: testSuggestions })).toEqual({
isIncomplete: false,
suggestions: [
{
documentation: 'Primitive: boolean',
insertText: 'boolean',
kind: 'type',
label: 'boolean',
},
{
documentation: 'Primitive: int',
insertText: 'int',
kind: 'type',
label: 'int',
},
{
documentation: 'Class: Math',
insertText: 'Math',
@ -122,6 +94,7 @@ describe('Autocomplete lib', () => {
label: 'ArithmeticException',
},
...keywords,
...getTypeSuggestions(),
],
});
});
@ -149,12 +122,6 @@ describe('Autocomplete lib', () => {
});
});
describe('getPrimitives()', () => {
test('returns primitive values', () => {
expect(getPrimitives(testSuggestions)).toEqual(['boolean', 'int']);
});
});
describe('getClassMemberSuggestions()', () => {
test('returns class member suggestions', () => {
expect(getClassMemberSuggestions(testSuggestions, 'Math')).toEqual({

View file

@ -27,7 +27,7 @@ import {
} from '../../types';
import {
painlessTestContext,
commonContext,
scoreContext,
filterContext,
booleanScriptFieldScriptFieldContext,
@ -81,6 +81,17 @@ export const getKeywords = (): PainlessCompletionItem[] => {
return allKeywords;
};
export const getTypeSuggestions = (): PainlessCompletionItem[] => {
return lexerRules.primitives.map((primitive) => {
return {
label: primitive,
kind: 'type',
documentation: `Type: ${primitive}`,
insertText: primitive,
};
});
};
const runtimeContexts: PainlessContext[] = [
'boolean_script_field_script_field',
'date_script_field',
@ -91,7 +102,7 @@ const runtimeContexts: PainlessContext[] = [
];
const mapContextToData: { [key: string]: { suggestions: any[] } } = {
painless_test: painlessTestContext,
painless_test: commonContext,
score: scoreContext,
filter: filterContext,
boolean_script_field_script_field: booleanScriptFieldScriptFieldContext,
@ -118,6 +129,7 @@ export const getStaticSuggestions = ({
});
const keywords = getKeywords();
const typeSuggestions = getTypeSuggestions();
let keywordSuggestions: PainlessCompletionItem[] = hasFields
? [
@ -156,14 +168,10 @@ export const getStaticSuggestions = ({
return {
isIncomplete: false,
suggestions: [...classSuggestions, ...keywordSuggestions],
suggestions: [...classSuggestions, ...keywordSuggestions, ...typeSuggestions],
};
};
export const getPrimitives = (suggestions: Suggestion[]): string[] => {
return suggestions.filter((suggestion) => suggestion.kind === 'type').map((type) => type.label);
};
export const getClassMemberSuggestions = (
suggestions: Suggestion[],
className: string
@ -224,10 +232,16 @@ export const getAutocompleteSuggestions = (
words: string[],
fields?: PainlessAutocompleteField[]
): PainlessCompletionResult => {
const suggestions = mapContextToData[painlessContext].suggestions;
// Unique suggestions based on context
const contextSuggestions = mapContextToData[painlessContext].suggestions;
// Enhance suggestions with common classes that exist in all contexts
// "painless_test" is the exception since it equals the common suggestions
const suggestions =
painlessContext === 'painless_test'
? contextSuggestions
: contextSuggestions.concat(commonContext.suggestions);
// What the user is currently typing
const activeTyping = words[words.length - 1];
const primitives = getPrimitives(suggestions);
// This logic may end up needing to be more robust as we integrate autocomplete into more editors
// For now, we're assuming there is a list of painless contexts that are only applicable in runtime fields
const isRuntimeContext = runtimeContexts.includes(painlessContext);
@ -247,7 +261,7 @@ export const getAutocompleteSuggestions = (
} else if (isAccessingProperty(activeTyping)) {
const className = activeTyping.substring(0, activeTyping.length - 1).split('.')[0];
autocompleteSuggestions = getClassMemberSuggestions(suggestions, className);
} else if (showStaticSuggestions(activeTyping, words, primitives)) {
} else if (showStaticSuggestions(activeTyping, words, lexerRules.primitives)) {
autocompleteSuggestions = getStaticSuggestions({ suggestions, hasFields, isRuntimeContext });
}
return autocompleteSuggestions;

View file

@ -37,8 +37,8 @@ export const isAccessingProperty = (activeTyping: string): boolean => {
* If the preceding word is a primitive type, e.g., "boolean",
* we assume the user is declaring a variable and will skip autocomplete
*
* Note: this isn't entirely exhaustive. For example, "def myVar =" is not included in context
* It's also acceptable to use a class as a type, e.g., "String myVar ="
* Note: this isn't entirely exhaustive.
* For example, you may use a class as a type, e.g., "String myVar ="
*/
export const hasDeclaredType = (activeLineWords: string[], primitives: string[]): boolean => {
return activeLineWords.length === 2 && primitives.includes(activeLineWords[0]);