@salesforce/soql-language-server
Version:
SOQL Language Server
492 lines • 24.8 kB
JavaScript
"use strict";
/*
* Copyright (c) 2021, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.findCursorTokenIndex = exports.lastX = exports.completionsFor = void 0;
const SoqlParser_1 = require("@salesforce/soql-common/lib/soql-parser/generated/SoqlParser");
const SoqlLexer_1 = require("@salesforce/soql-common/lib/soql-parser/generated/SoqlLexer");
const soql_parser_1 = require("@salesforce/soql-common/lib/soql-parser");
const vscode_languageserver_1 = require("vscode-languageserver");
const antlr4ts_1 = require("antlr4ts");
const c3 = require("antlr4-c3");
const soqlComments_1 = require("@salesforce/soql-common/lib/soqlComments");
const soql_functions_1 = require("./completion/soql-functions");
const SoqlCompletionErrorStrategy_1 = require("./completion/SoqlCompletionErrorStrategy");
const soql_query_analysis_1 = require("./completion/soql-query-analysis");
const SOBJECTS_ITEM_LABEL_PLACEHOLDER = '__SOBJECTS_PLACEHOLDER';
const SOBJECT_FIELDS_LABEL_PLACEHOLDER = '__SOBJECT_FIELDS_PLACEHOLDER';
const RELATIONSHIPS_PLACEHOLDER = '__RELATIONSHIPS_PLACEHOLDER';
const RELATIONSHIP_FIELDS_PLACEHOLDER = '__RELATIONSHIP_FIELDS_PLACEHOLDER';
const LITERAL_VALUES_FOR_FIELD = '__LITERAL_VALUES_FOR_FIELD';
const UPDATE_TRACKING = 'UPDATE TRACKING';
const UPDATE_VIEWSTAT = 'UPDATE VIEWSTAT';
const DEFAULT_SOBJECT = 'Object';
const itemsForBuiltinFunctions = soql_functions_1.soqlFunctions.map((soqlFn) => newFunctionItem(soqlFn.name));
function completionsFor(text, line, column) {
const lexer = new SoqlLexer_1.SoqlLexer(new soql_parser_1.LowerCasingCharStream(soqlComments_1.parseHeaderComments(text).headerPaddedSoqlText));
const tokenStream = new antlr4ts_1.CommonTokenStream(lexer);
const parser = new SoqlParser_1.SoqlParser(tokenStream);
parser.removeErrorListeners();
parser.errorHandler = new SoqlCompletionErrorStrategy_1.SoqlCompletionErrorStrategy();
const parsedQuery = parser.soqlQuery();
const completionTokenIndex = findCursorTokenIndex(tokenStream, {
line,
column,
});
if (completionTokenIndex === undefined) {
// eslint-disable-next-line no-console
console.error("Couldn't find cursor position on toke stream! Lexer might be skipping some tokens!");
return [];
}
const c3Candidates = collectC3CompletionCandidates(parser, parsedQuery, completionTokenIndex);
const soqlQueryAnalyzer = new soql_query_analysis_1.SoqlQueryAnalyzer(parsedQuery);
const itemsFromTokens = generateCandidatesFromTokens(c3Candidates.tokens, soqlQueryAnalyzer, lexer, tokenStream, completionTokenIndex);
const itemsFromRules = generateCandidatesFromRules(c3Candidates.rules, soqlQueryAnalyzer, tokenStream, completionTokenIndex);
const completionItems = itemsFromTokens.concat(itemsFromRules);
// If we got no proposals from C3, handle some special cases "manually"
return handleSpecialCases(soqlQueryAnalyzer, tokenStream, completionTokenIndex, completionItems);
}
exports.completionsFor = completionsFor;
function collectC3CompletionCandidates(parser, parsedQuery, completionTokenIndex) {
const core = new c3.CodeCompletionCore(parser);
core.translateRulesTopDown = false;
core.ignoredTokens = new Set([
SoqlLexer_1.SoqlLexer.BIND,
SoqlLexer_1.SoqlLexer.LPAREN,
SoqlLexer_1.SoqlLexer.DISTANCE,
SoqlLexer_1.SoqlLexer.COMMA,
SoqlLexer_1.SoqlLexer.PLUS,
SoqlLexer_1.SoqlLexer.MINUS,
SoqlLexer_1.SoqlLexer.COLON,
SoqlLexer_1.SoqlLexer.MINUS,
]);
core.preferredRules = new Set([
SoqlParser_1.SoqlParser.RULE_soqlFromExprs,
SoqlParser_1.SoqlParser.RULE_soqlFromExpr,
SoqlParser_1.SoqlParser.RULE_soqlField,
SoqlParser_1.SoqlParser.RULE_soqlUpdateStatsClause,
SoqlParser_1.SoqlParser.RULE_soqlIdentifier,
SoqlParser_1.SoqlParser.RULE_soqlLiteralValue,
SoqlParser_1.SoqlParser.RULE_soqlLikeLiteral,
]);
return core.collectCandidates(completionTokenIndex, parsedQuery);
}
function lastX(array) {
return array && array.length > 0 ? array[array.length - 1] : undefined;
}
exports.lastX = lastX;
const possibleIdentifierPrefix = /[\w]$/;
const lineSeparator = /\n|\r|\r\n/g;
/**
* @returns the token index for which we want to provide completion candidates,
* which depends on the cursor possition.
*
* @example
* ```soql
* SELECT id| FROM x : Cursor touching the previous identifier token:
* we want to continue completing that prior token position
* SELECT id |FROM x : Cursor NOT touching the previous identifier token:
* we want to complete what comes on this new position
* SELECT id | FROM x : Cursor within whitespace block: we want to complete what
* comes after the whitespace (we must return a non-WS token index)
* ```
*/
function findCursorTokenIndex(tokenStream, cursor) {
var _a;
// NOTE: cursor position is 1-based, while token's charPositionInLine is 0-based
const cursorCol = cursor.column - 1;
for (let i = 0; i < tokenStream.size; i++) {
const t = tokenStream.get(i);
const tokenStartCol = t.charPositionInLine;
const tokenEndCol = tokenStartCol + t.text.length;
const tokenStartLine = t.line;
const tokenEndLine = t.type !== SoqlLexer_1.SoqlLexer.WS || !t.text ? tokenStartLine : tokenStartLine + (((_a = t.text.match(lineSeparator)) === null || _a === void 0 ? void 0 : _a.length) || 0);
// NOTE: tokenEndCol makes sense only of tokenStartLine === tokenEndLine
if (tokenEndLine > cursor.line || (tokenStartLine === cursor.line && tokenEndCol > cursorCol)) {
if (i > 0 &&
tokenStartLine === cursor.line &&
tokenStartCol === cursorCol &&
possibleIdentifierPrefix.test(tokenStream.get(i - 1).text)) {
return i - 1;
}
else if (tokenStream.get(i).type === SoqlLexer_1.SoqlLexer.WS) {
return i + 1;
}
else
return i;
}
}
return undefined;
}
exports.findCursorTokenIndex = findCursorTokenIndex;
function tokenTypeToCandidateString(lexer, tokenType) {
var _a;
return (_a = lexer.vocabulary.getLiteralName(tokenType)) === null || _a === void 0 ? void 0 : _a.toUpperCase().replace(/^'|'$/g, '');
}
const fieldDependentOperators = new Set([
SoqlLexer_1.SoqlLexer.LT,
SoqlLexer_1.SoqlLexer.GT,
SoqlLexer_1.SoqlLexer.INCLUDES,
SoqlLexer_1.SoqlLexer.EXCLUDES,
SoqlLexer_1.SoqlLexer.LIKE,
]);
function generateCandidatesFromTokens(tokens, soqlQueryAnalyzer, lexer, tokenStream, tokenIndex) {
const items = [];
for (const [tokenType, followingTokens] of tokens) {
// Don't propose what's already at the cursor position
if (tokenType === tokenStream.get(tokenIndex).type) {
continue;
}
// Even though the grammar allows spaces between the < > and = signs
// (for example, this is valid: `field < = 'value'`), we don't want to
// propose code completions like that
if (tokenType === SoqlLexer_1.SoqlLexer.EQ && isCursorAfter(tokenStream, tokenIndex, [[SoqlLexer_1.SoqlLexer.LT, SoqlLexer_1.SoqlLexer.GT]])) {
continue;
}
const baseKeyword = tokenTypeToCandidateString(lexer, tokenType);
if (!baseKeyword)
continue;
const followingKeywords = followingTokens.map((t) => tokenTypeToCandidateString(lexer, t)).join(' ');
let itemText = followingKeywords.length > 0 ? baseKeyword + ' ' + followingKeywords : baseKeyword;
// No aggregate features on nested queries
const queryInfos = soqlQueryAnalyzer.queryInfosAt(tokenIndex);
if (queryInfos.length > 1 && (itemText === 'COUNT' || itemText === 'GROUP BY')) {
continue;
}
let soqlItemContext;
if (fieldDependentOperators.has(tokenType)) {
const soqlFieldExpr = soqlQueryAnalyzer.extractWhereField(tokenIndex);
if (soqlFieldExpr) {
soqlItemContext = {
sobjectName: soqlFieldExpr.sobjectName,
fieldName: soqlFieldExpr.fieldName,
};
const soqlOperator = soql_functions_1.soqlOperators[itemText];
soqlItemContext.onlyTypes = soqlOperator.types;
}
}
// Some "manual" improvements for some keywords:
if (['IN', 'NOT IN', 'INCLUDES', 'EXCLUDES'].includes(itemText)) {
itemText = itemText + ' (';
}
else if (itemText === 'COUNT') {
// NOTE: The g4 grammar declares `COUNT()` explicitly, but not `COUNT(xyz)`.
// Here we cover the first case:
itemText = 'COUNT()';
}
const newItem = soqlItemContext
? withSoqlContext(newKeywordItem(itemText), soqlItemContext)
: newKeywordItem(itemText);
if (itemText === 'WHERE') {
newItem.preselect = true;
}
items.push(newItem);
// Clone extra related operators missing by C3 proposals
if (['<', '>'].includes(itemText)) {
items.push({ ...newItem, ...newKeywordItem(itemText + '=') });
}
if (itemText === '=') {
items.push({ ...newItem, ...newKeywordItem('!=') });
items.push({ ...newItem, ...newKeywordItem('<>') });
}
}
return items;
}
// eslint-disable-next-line complexity
function generateCandidatesFromRules(c3Rules, soqlQueryAnalyzer, tokenStream, tokenIndex) {
const completionItems = [];
const queryInfos = soqlQueryAnalyzer.queryInfosAt(tokenIndex);
const innermostQueryInfo = queryInfos.length > 0 ? queryInfos[0] : undefined;
const fromSObject = (innermostQueryInfo === null || innermostQueryInfo === void 0 ? void 0 : innermostQueryInfo.sobjectName) || DEFAULT_SOBJECT;
const soqlItemContext = {
sobjectName: fromSObject,
};
const isInnerQuery = queryInfos.length > 1;
const relationshipName = isInnerQuery ? queryInfos[0].sobjectName : undefined;
const parentQuerySObject = isInnerQuery ? queryInfos[1].sobjectName : undefined;
for (const [ruleId, ruleData] of c3Rules) {
const lastRuleId = ruleData.ruleList[ruleData.ruleList.length - 1];
switch (ruleId) {
case SoqlParser_1.SoqlParser.RULE_soqlUpdateStatsClause:
// NOTE: We handle this one as a Rule instead of Tokens because
// "TRACKING" and "VIEWSTAT" are not part of the grammar
if (tokenIndex === ruleData.startTokenIndex) {
completionItems.push(newKeywordItem(UPDATE_TRACKING));
completionItems.push(newKeywordItem(UPDATE_VIEWSTAT));
}
break;
case SoqlParser_1.SoqlParser.RULE_soqlFromExprs:
if (tokenIndex === ruleData.startTokenIndex) {
completionItems.push(...itemsForFromExpression(soqlQueryAnalyzer, tokenIndex));
}
break;
case SoqlParser_1.SoqlParser.RULE_soqlField:
if (lastRuleId === SoqlParser_1.SoqlParser.RULE_soqlSemiJoin) {
completionItems.push(withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), {
...soqlItemContext,
onlyTypes: ['id', 'reference'],
dontShowRelationshipField: true,
}));
}
else if (lastRuleId === SoqlParser_1.SoqlParser.RULE_soqlSelectExpr) {
const isCursorAtFunctionExpr = isCursorAfter(tokenStream, tokenIndex, [
[SoqlLexer_1.SoqlLexer.IDENTIFIER, SoqlLexer_1.SoqlLexer.COUNT],
[SoqlLexer_1.SoqlLexer.LPAREN],
]); // inside a function expression (i.e.: "SELECT AVG(|" )
// SELECT | FROM Xyz
if (tokenIndex === ruleData.startTokenIndex) {
if (isInnerQuery) {
completionItems.push(withSoqlContext(newFieldItem(RELATIONSHIP_FIELDS_PLACEHOLDER), {
...soqlItemContext,
sobjectName: parentQuerySObject || '',
relationshipName,
}));
}
else {
completionItems.push(withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), soqlItemContext));
completionItems.push(...itemsForBuiltinFunctions);
completionItems.push(newSnippetItem('(SELECT ... FROM ...)', '(SELECT $2 FROM $1)'));
}
}
// "SELECT AVG(|"
else if (isCursorAtFunctionExpr) {
// NOTE: This code would be simpler if the grammar had an explicit
// rule for function invocation.
// It's also more complicated because COUNT is a keyword type in the grammar,
// and not an IDENTIFIER like all other functions
const functionNameToken = searchTokenBeforeCursor(tokenStream, tokenIndex, [
SoqlLexer_1.SoqlLexer.IDENTIFIER,
SoqlLexer_1.SoqlLexer.COUNT,
]);
if (functionNameToken) {
const soqlFn = soql_functions_1.soqlFunctionsByName[(functionNameToken === null || functionNameToken === void 0 ? void 0 : functionNameToken.text) || ''];
if (soqlFn) {
soqlItemContext.onlyAggregatable = soqlFn.isAggregate;
soqlItemContext.onlyTypes = soqlFn.types;
}
}
completionItems.push(withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), soqlItemContext));
}
}
// ... GROUP BY |
else if (lastRuleId === SoqlParser_1.SoqlParser.RULE_soqlGroupByExprs && tokenIndex === ruleData.startTokenIndex) {
const selectedFields = (innermostQueryInfo === null || innermostQueryInfo === void 0 ? void 0 : innermostQueryInfo.selectedFields) || [];
const groupedByFields = ((innermostQueryInfo === null || innermostQueryInfo === void 0 ? void 0 : innermostQueryInfo.groupByFields) || []).map((f) => f.toLowerCase());
const groupFieldDifference = selectedFields.filter((f) => !groupedByFields.includes(f.toLowerCase()));
completionItems.push(withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), {
sobjectName: fromSObject,
onlyGroupable: true,
mostLikelyItems: groupFieldDifference.length > 0 ? groupFieldDifference : undefined,
}));
}
// ... ORDER BY |
else if (lastRuleId === SoqlParser_1.SoqlParser.RULE_soqlOrderByClauseField) {
completionItems.push(isInnerQuery
? withSoqlContext(newFieldItem(RELATIONSHIP_FIELDS_PLACEHOLDER), {
...soqlItemContext,
sobjectName: parentQuerySObject || '',
relationshipName,
onlySortable: true,
})
: withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), {
...soqlItemContext,
onlySortable: true,
}));
}
break;
// For some reason, c3 doesn't propose rule `soqlField` when inside soqlWhereExpr,
// but it does propose soqlIdentifier, so we hinge off it for where expressions
case SoqlParser_1.SoqlParser.RULE_soqlIdentifier:
if (tokenIndex === ruleData.startTokenIndex &&
[SoqlParser_1.SoqlParser.RULE_soqlWhereExpr, SoqlParser_1.SoqlParser.RULE_soqlDistanceExpr].includes(lastRuleId) &&
!ruleData.ruleList.includes(SoqlParser_1.SoqlParser.RULE_soqlHavingClause)) {
completionItems.push(withSoqlContext(newFieldItem(SOBJECT_FIELDS_LABEL_PLACEHOLDER), {
sobjectName: fromSObject,
}));
}
break;
case SoqlParser_1.SoqlParser.RULE_soqlLiteralValue:
case SoqlParser_1.SoqlParser.RULE_soqlLikeLiteral:
if (!ruleData.ruleList.includes(SoqlParser_1.SoqlParser.RULE_soqlHavingClause)) {
const soqlFieldExpr = soqlQueryAnalyzer.extractWhereField(tokenIndex);
if (soqlFieldExpr) {
for (const literalItem of createItemsForLiterals(soqlFieldExpr))
completionItems.push(literalItem);
}
}
break;
}
}
return completionItems;
}
function handleSpecialCases(soqlQueryAnalyzer, tokenStream, tokenIndex, completionItems) {
if (completionItems.length === 0) {
// SELECT FROM |
if (isCursorAfter(tokenStream, tokenIndex, [[SoqlLexer_1.SoqlLexer.SELECT], [SoqlLexer_1.SoqlLexer.FROM]])) {
completionItems.push(...itemsForFromExpression(soqlQueryAnalyzer, tokenIndex));
}
}
// Provide smart snippet for `SELECT`:
if (completionItems.some((item) => item.label === 'SELECT')) {
if (!isCursorBefore(tokenStream, tokenIndex, [[SoqlLexer_1.SoqlLexer.FROM]])) {
completionItems.push(newSnippetItem('SELECT ... FROM ...', 'SELECT $2 FROM $1'));
}
}
return completionItems;
}
function itemsForFromExpression(soqlQueryAnalyzer, tokenIndex) {
const completionItems = [];
const queryInfoStack = soqlQueryAnalyzer.queryInfosAt(tokenIndex);
if (queryInfoStack.length === 1 || (queryInfoStack.length > 1 && queryInfoStack[0].isSemiJoin)) {
completionItems.push(newObjectItem(SOBJECTS_ITEM_LABEL_PLACEHOLDER));
}
else if (queryInfoStack.length > 1) {
const parentQuery = queryInfoStack[1];
const sobjectName = parentQuery.sobjectName;
if (sobjectName) {
// NOTE: might need to pass multiple outter SObject (nested) names ?
completionItems.push(withSoqlContext(newObjectItem(RELATIONSHIPS_PLACEHOLDER), {
sobjectName,
}));
}
}
return completionItems;
}
function isCursorAfter(tokenStream, tokenIndex, matchingTokens) {
const toMatch = matchingTokens.concat().reverse();
let matchingIndex = 0;
for (let i = tokenIndex - 1; i >= 0; i--) {
const t = tokenStream.get(i);
if (t.channel === SoqlLexer_1.SoqlLexer.HIDDEN)
continue;
if (toMatch[matchingIndex].includes(t.type)) {
matchingIndex++;
if (matchingIndex === toMatch.length)
return true;
}
else
break;
}
return false;
}
function isCursorBefore(tokenStream, tokenIndex, matchingTokens) {
const toMatch = matchingTokens.concat();
let matchingIndex = 0;
for (let i = tokenIndex; i < tokenStream.size; i++) {
const t = tokenStream.get(i);
if (t.channel === SoqlLexer_1.SoqlLexer.HIDDEN)
continue;
if (toMatch[matchingIndex].includes(t.type)) {
matchingIndex++;
if (matchingIndex === toMatch.length)
return true;
}
else
break;
}
return false;
}
function searchTokenBeforeCursor(tokenStream, tokenIndex, searchForAnyTokenTypes) {
for (let i = tokenIndex - 1; i >= 0; i--) {
const t = tokenStream.get(i);
if (t.channel === SoqlLexer_1.SoqlLexer.HIDDEN)
continue;
if (searchForAnyTokenTypes.includes(t.type)) {
return t;
}
}
return undefined;
}
function newKeywordItem(text) {
return {
label: text,
kind: vscode_languageserver_1.CompletionItemKind.Keyword,
};
}
function newFunctionItem(text) {
return {
label: text + '(...)',
kind: vscode_languageserver_1.CompletionItemKind.Function,
insertText: text + '($1)',
insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
};
}
function withSoqlContext(item, soqlItemCtx) {
item.data = { soqlContext: soqlItemCtx };
return item;
}
const newCompletionItem = (text, kind, extraOptions) => ({
label: text,
kind,
...extraOptions,
});
const newFieldItem = (text, extraOptions) => newCompletionItem(text, vscode_languageserver_1.CompletionItemKind.Field, extraOptions);
const newConstantItem = (text) => newCompletionItem(text, vscode_languageserver_1.CompletionItemKind.Constant);
const newObjectItem = (text) => newCompletionItem(text, vscode_languageserver_1.CompletionItemKind.Class);
const newSnippetItem = (label, snippet, extraOptions) => newCompletionItem(label, vscode_languageserver_1.CompletionItemKind.Snippet, {
insertText: snippet,
insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
...extraOptions,
});
function createItemsForLiterals(soqlFieldExpr) {
var _a;
const soqlContext = {
sobjectName: soqlFieldExpr.sobjectName,
fieldName: soqlFieldExpr.fieldName,
};
const items = [
withSoqlContext(newCompletionItem('TRUE', vscode_languageserver_1.CompletionItemKind.Value), {
...soqlContext,
...{ onlyTypes: ['boolean'] },
}),
withSoqlContext(newCompletionItem('FALSE', vscode_languageserver_1.CompletionItemKind.Value), {
...soqlContext,
...{ onlyTypes: ['boolean'] },
}),
withSoqlContext(newSnippetItem('nnn', '${1:123}'), {
...soqlContext,
...{ onlyTypes: ['int'] },
}),
withSoqlContext(newSnippetItem('nnn.nnn', '${1:123.456}'), {
...soqlContext,
...{ onlyTypes: ['double'] },
}),
withSoqlContext(newSnippetItem('ISOCODEnnn.nn', '${1|USD,EUR,JPY,CNY,CHF|}${2:999.99}'), {
...soqlContext,
...{ onlyTypes: ['currency'] },
}),
withSoqlContext(newSnippetItem('abc123', "'${1:abc123}'"), {
...soqlContext,
...{ onlyTypes: ['string'] },
}),
withSoqlContext(newSnippetItem('YYYY-MM-DD', '${1:${CURRENT_YEAR}}-${2:${CURRENT_MONTH}}-${3:${CURRENT_DATE}}$0',
// extra space prefix on sortText to make it appear first:
{ preselect: true, sortText: ' YYYY-MM-DD' }), { ...soqlContext, ...{ onlyTypes: ['date'] } }),
withSoqlContext(newSnippetItem('YYYY-MM-DDThh:mm:ssZ', '${1:${CURRENT_YEAR}}-${2:${CURRENT_MONTH}}-${3:${CURRENT_DATE}}T${4:${CURRENT_HOUR}}:${5:${CURRENT_MINUTE}}:${6:${CURRENT_SECOND}}Z$0',
// extra space prefix on sortText to make it appear first:
{ preselect: true, sortText: ' YYYY-MM-DDThh:mm:ssZ' }), { ...soqlContext, ...{ onlyTypes: ['datetime'] } }),
...soql_functions_1.soqlDateRangeLiterals.map((k) => withSoqlContext(newCompletionItem(k, vscode_languageserver_1.CompletionItemKind.Value), {
...soqlContext,
...{ onlyTypes: ['date', 'datetime'] },
})),
...soql_functions_1.soqlParametricDateRangeLiterals.map((k) => withSoqlContext(newSnippetItem(k, k.replace(':n', ':${1:nn}') + '$0'), {
...soqlContext,
...{ onlyTypes: ['date', 'datetime'] },
})),
// Give the LSP client a chance to add additional literals:
withSoqlContext(newConstantItem(LITERAL_VALUES_FOR_FIELD), soqlContext),
];
const notNillableOperator = Boolean(soqlFieldExpr.operator !== undefined && ((_a = soql_functions_1.soqlOperators[soqlFieldExpr.operator]) === null || _a === void 0 ? void 0 : _a.notNullable));
if (!notNillableOperator) {
items.push(withSoqlContext(newKeywordItem('NULL'), {
...soqlContext,
...{ onlyNillable: true },
}));
}
return items;
}
//# sourceMappingURL=completion.js.map