UNPKG

ifc-expressions

Version:

Parsing and evaluation of IFC expressions

655 lines (654 loc) 26.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IfcExpressionAutocomplete = void 0; const antlr4ng_1 = require("antlr4ng"); const antlr4_c3_1 = require("antlr4-c3"); const BuiltinVariableRegistry_js_1 = require("../builtin/BuiltinVariableRegistry.js"); const IfcExpressionLexer_js_1 = require("../gen/parser/IfcExpressionLexer.js"); const IfcExpressionParser_js_1 = require("../gen/parser/IfcExpressionParser.js"); const IfcExpressionFunctions_js_1 = require("../expression/function/IfcExpressionFunctions.js"); const ContextObjectType_js_1 = require("../type/ContextObjectType.js"); const Documentation_js_1 = require("../documentation/Documentation.js"); function isIdentifierChar(char) { return typeof char === "string" && /[a-zA-Z0-9_\-$&]/.test(char); } function toBuiltinLabel(name) { return name.startsWith("$") ? name : `$${name}`; } function normalizeForMatch(value) { return value.toUpperCase(); } function toCompletionTypeName(type) { return type.getName().replace(/^\$/, ""); } function isChainableType(type) { return type instanceof ContextObjectType_js_1.ContextObjectType; } const unsupportedPrimitiveMethodFunctionNames = new Set([ "NAME", "GUID", "IFCCLASS", "DESCRIPTION", "VALUE", ]); const preferredMethodLabels = new Map([ ["MAP", "map"], ["CHOOSE", "choose"], ["AT", "at"], ["IF", "if"], ["ROUND", "round"], ["NAME", "name"], ["GUID", "guid"], ["IFCCLASS", "ifcClass"], ["DESCRIPTION", "description"], ["VALUE", "value"], ["PROPERTYSET", "propertySet"], ["PROPERTY", "property"], ["TYPE", "type"], ["NOT", "not"], ["TOSTRING", "toString"], ["TONUMERIC", "toNumeric"], ["TONUMBER", "toNumber"], ["TOBOOLEAN", "toBoolean"], ["TOLOGICAL", "toLogical"], ["NOTFOUNDASUNKNOWN", "notFoundAsUnknown"], ["TOIFCDATE", "toIfcDate"], ["TOIFCTIME", "toIfcTime"], ["TOIFCDATETIME", "toIfcDateTime"], ["TOIFCDURATION", "toIfcDuration"], ["TOIFCTIMESTAMP", "toIfcTimeStamp"], ["ADDDURATION", "addDuration"], ["TOLOWERCASE", "toLowerCase"], ["TOUPPERCASE", "toUpperCase"], ["SUBSTRING", "substring"], ["SPLIT", "split"], ["EXISTS", "exists"], ["AND", "and"], ["OR", "or"], ["XOR", "xor"], ["IMPLIES", "implies"], ["EQUALS", "equals"], ["GREATERTHAN", "greaterThan"], ["GREATERTHANOREQUAL", "greaterThanOrEqual"], ["LESSTHAN", "lessThan"], ["LESSTHANOREQUAL", "lessThanOrEqual"], ["CONTAINS", "contains"], ["MATCHES", "matches"], ["REGEXCONTAINS", "regexContains"], ["REGEXMATCHES", "regexMatches"], ["REPLACE", "replace"], ["REGEXREPLACE", "regexReplace"], ]); const emptyExpressionStarterFunctionNames = [ "IF", "EXISTS", "ROUND", "CONTAINS", "MATCHES", "REPLACE", "TOSTRING", "TONUMERIC", "TOBOOLEAN", ]; function isExtendableTokenType(tokenType) { return tokenType === IfcExpressionParser_js_1.IfcExpressionParser.IDENTIFIER; } function findCaretTokenIndex(tokens, cursorOffset) { if (tokens.length === 0) { return 0; } const cursor = Math.max(0, cursorOffset); for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token.type === antlr4ng_1.Token.EOF) { return token.tokenIndex; } if (cursor < token.start) { return token.tokenIndex; } if (cursor <= token.stop) { return token.tokenIndex; } if (cursor === token.stop + 1) { if (isExtendableTokenType(token.type)) { return token.tokenIndex; } const nextToken = tokens[i + 1]; return nextToken?.tokenIndex ?? token.tokenIndex; } } return tokens[tokens.length - 1].tokenIndex; } function parseAutocompleteInput(input, cursorOffset) { const chars = antlr4ng_1.CharStream.fromString(input); const lexer = new IfcExpressionLexer_js_1.IfcExpressionLexer(chars); const tokenStream = new antlr4ng_1.CommonTokenStream(lexer); const parser = new IfcExpressionParser_js_1.IfcExpressionParser(tokenStream); lexer.removeErrorListeners(); parser.removeErrorListeners(); const parseTree = parser.expr(); tokenStream.fill(); const tokens = tokenStream.getTokens(); return { parser, parseTree, tokens, caretTokenIndex: findCaretTokenIndex(tokens, cursorOffset), }; } function findBuiltinRootReplaceRange(input, cursorOffset) { const cursor = Math.max(0, Math.min(cursorOffset, input.length)); let i = cursor - 1; while (i >= 0 && isIdentifierChar(input[i]) && input[i] !== "$") { i--; } if (i >= 0 && input[i] === "$") { return { from: i, to: cursor }; } return undefined; } function findIdentifierReplaceRange(input, cursorOffset) { const cursor = Math.max(0, Math.min(cursorOffset, input.length)); let from = cursor; while (from > 0 && isIdentifierChar(input[from - 1])) { from--; } return { from, to: cursor, }; } function findMemberReplaceRange(input, cursorOffset) { const range = findIdentifierReplaceRange(input, cursorOffset); if (range.from === 0 || input[range.from - 1] !== ".") { return undefined; } return range; } function findFunctionReplaceRange(input, cursorOffset) { const range = findIdentifierReplaceRange(input, cursorOffset); const prefixChar = range.from > 0 ? input[range.from - 1] : undefined; const hasIdentifierFragment = range.from < range.to || isIdentifierChar(input[Math.max(0, range.to)]); if (!hasIdentifierFragment || prefixChar === "." || prefixChar === "$") { return undefined; } return range; } function countExprListArguments(exprList) { if (!exprList) { return 0; } const rest = exprList.exprList(); return 1 + (rest ? countExprListArguments(rest) : 0); } function toMethodAccessorStep(methodAccessor) { if (methodAccessor instanceof IfcExpressionParser_js_1.MethodPropertyAccessContext) { const identifier = methodAccessor.IDENTIFIER()?.getText(); if (!identifier) { return undefined; } return { name: identifier, kind: "property", startTokenIndex: methodAccessor.start?.tokenIndex ?? -1, }; } if (methodAccessor instanceof IfcExpressionParser_js_1.MethodFunctionCallContext) { const functionCall = methodAccessor.functionCall(); const name = functionCall.IDENTIFIER()?.getText(); if (!name) { return undefined; } return { name, kind: "function", argumentCount: countExprListArguments(functionCall.exprList()), startTokenIndex: functionCall.start?.tokenIndex ?? -1, }; } return undefined; } function collectMethodCallChainSteps(methodCallChain) { if (methodCallChain instanceof IfcExpressionParser_js_1.MethodCallChainInnerContext) { const current = toMethodAccessorStep(methodCallChain.methodAccessor()); const rest = collectMethodCallChainSteps(methodCallChain.methodCallChain()); if (!current || !rest) { return undefined; } return [current, ...rest]; } if (methodCallChain instanceof IfcExpressionParser_js_1.MethodCallChainEndContext) { const current = toMethodAccessorStep(methodCallChain.methodAccessor()); return current ? [current] : undefined; } const dot = methodCallChain.getToken(IfcExpressionParser_js_1.IfcExpressionParser.DOT, 0); if (dot) { return [ { kind: "incomplete", startTokenIndex: dot.symbol.tokenIndex + 1, }, ]; } return []; } function getMemberResultType(definition) { return definition.kind === "property" ? definition.valueType : definition.returnType; } function resolveCompletedAccessorStep(currentType, step) { if (!(currentType instanceof ContextObjectType_js_1.ContextObjectType)) { return undefined; } const memberDefinition = currentType.getMemberDefinition(step.name); if (!memberDefinition) { return undefined; } if (step.kind === "property") { return memberDefinition.kind === "property" ? memberDefinition.valueType : undefined; } if (memberDefinition.kind !== "function") { return undefined; } if (memberDefinition.argumentTypes.length !== step.argumentCount) { return undefined; } return memberDefinition.returnType; } function resolveSingleExprType(singleExpr, builtinVariableRegistry) { if (singleExpr instanceof IfcExpressionParser_js_1.SEVariableRefContext) { const identifier = singleExpr.variableRef()?.IDENTIFIER()?.getText(); return identifier ? builtinVariableRegistry.getDefinition(`$${identifier}`)?.type : undefined; } if (singleExpr instanceof IfcExpressionParser_js_1.SEParenthesisContext) { return resolveSingleExprType(singleExpr.singleExpr(), builtinVariableRegistry); } if (singleExpr instanceof IfcExpressionParser_js_1.SEMethodCallContext) { const baseType = resolveSingleExprType(singleExpr.singleExpr(), builtinVariableRegistry); const steps = collectMethodCallChainSteps(singleExpr.methodCallChain()); if (!baseType || !steps) { return undefined; } let currentType = baseType; for (const step of steps) { if (step.kind === "incomplete") { return undefined; } currentType = resolveCompletedAccessorStep(currentType, step); if (!currentType) { return undefined; } } return currentType; } return undefined; } function findReceiverTypeForMemberSlot(node, startTokenIndex, builtinVariableRegistry) { if (node instanceof IfcExpressionParser_js_1.SEMethodCallContext) { const steps = collectMethodCallChainSteps(node.methodCallChain()); if (steps) { const slotIndex = steps.findIndex((step) => step.startTokenIndex === startTokenIndex); if (slotIndex >= 0) { let currentType = resolveSingleExprType(node.singleExpr(), builtinVariableRegistry); for (const step of steps.slice(0, slotIndex)) { if (!currentType || step.kind === "incomplete") { return undefined; } currentType = resolveCompletedAccessorStep(currentType, step); } return currentType; } } } for (let i = 0; i < node.getChildCount(); i++) { const child = node.getChild(i); if (child instanceof antlr4ng_1.ParserRuleContext) { const receiverType = findReceiverTypeForMemberSlot(child, startTokenIndex, builtinVariableRegistry); if (receiverType) { return receiverType; } } } return undefined; } function isEmptyExpressionInput(input) { return input.trim().length === 0; } function toFunctionInsertText(name) { return `${name}()`; } function buildSignatureLabel(name, argumentLabels) { return `${name}(${argumentLabels.join(", ")})`; } function buildFunctionDocumentation(name, localizer, displayName = name) { const func = IfcExpressionFunctions_js_1.IfcExpressionFunctions.getFunction(name); const documentation = func?.getDocumentation(); if (!func || !documentation) { return undefined; } const fallback = `${func.getSignatureLabel(displayName)}: ${documentation.fallback}`; return localizer ? localizer.t(documentation.key, fallback) : fallback; } function buildMemberDocumentation(definition, localizer) { if (!definition.documentation) { return undefined; } const fallback = definition.kind === "property" ? `${definition.name}: ${definition.documentation.fallback}` : `${buildSignatureLabel(definition.name, (definition.argumentDocumentation ?? []).map((argument, index) => argument.label.fallback ?? `arg${index}`))}: ${definition.documentation.fallback}`; return localizer ? localizer.t(definition.documentation.key, fallback) : fallback; } function buildRootDocumentation(name, documentation, localizer) { const key = documentation?.key ?? `builtin.${name}.summary`; const fallback = documentation?.fallback ?? `${name}: built-in value from the evaluation context`; return localizer ? localizer.t(key, fallback) : fallback; } function findActiveCallFrame(tokens, cursorOffset) { const stack = []; let previousSignificantToken; for (const token of tokens) { if (token.type === antlr4ng_1.Token.EOF || token.start >= cursorOffset) { break; } if (token.type === IfcExpressionParser_js_1.IfcExpressionParser.WS || token.type === IfcExpressionParser_js_1.IfcExpressionParser.NEWLINE) { continue; } if (token.type === IfcExpressionParser_js_1.IfcExpressionParser.T__1) { stack.push(previousSignificantToken?.type === IfcExpressionParser_js_1.IfcExpressionParser.IDENTIFIER ? { kind: "function", name: previousSignificantToken.text ?? undefined, argumentIndex: 0, startTokenIndex: previousSignificantToken.tokenIndex, } : { kind: "group", argumentIndex: 0 }); } else if (token.type === IfcExpressionParser_js_1.IfcExpressionParser.T__10) { stack.push({ kind: "array", argumentIndex: 0 }); } else if (token.type === IfcExpressionParser_js_1.IfcExpressionParser.T__2 || token.type === IfcExpressionParser_js_1.IfcExpressionParser.T__11) { stack.pop(); } else if (token.type === IfcExpressionParser_js_1.IfcExpressionParser.T__9) { const currentFrame = stack[stack.length - 1]; if (currentFrame?.kind === "function") { currentFrame.argumentIndex += 1; } } previousSignificantToken = token; } for (let i = stack.length - 1; i >= 0; i--) { if (stack[i].kind === "function") { return stack[i]; } } return undefined; } function buildMemberSignatureLabel(definition) { return buildSignatureLabel(definition.name, (definition.argumentDocumentation ?? []).map((argument, index) => argument.label.fallback ?? `arg${index}`)); } function buildActiveMemberHelp(parseTree, builtinVariableRegistry, activeFrame, localizer) { if (activeFrame.startTokenIndex === undefined || !activeFrame.name) { return undefined; } const receiverType = findReceiverTypeForMemberSlot(parseTree, activeFrame.startTokenIndex, builtinVariableRegistry); if (!(receiverType instanceof ContextObjectType_js_1.ContextObjectType)) { return undefined; } const memberDefinition = receiverType.getMemberDefinition(activeFrame.name); if (memberDefinition?.kind !== "function" || !memberDefinition.documentation) { return undefined; } const label = buildMemberSignatureLabel(memberDefinition); const fallback = memberDefinition.documentation.fallback.startsWith(`${label}: `) ? memberDefinition.documentation.fallback : `${label}: ${memberDefinition.documentation.fallback}`; const activeArgument = memberDefinition.argumentDocumentation?.[activeFrame.argumentIndex]; return { label, documentation: localizer ? localizer.t(memberDefinition.documentation.key, fallback) : fallback, activeParameterIndex: activeFrame.argumentIndex, activeParameterLabel: activeArgument ? (0, Documentation_js_1.resolveLocalizedText)(activeArgument.label, localizer) ?? activeArgument.label.fallback : undefined, activeParameterDocumentation: activeArgument ? (0, Documentation_js_1.resolveLocalizedText)(activeArgument.documentation, localizer) : undefined, }; } function buildActiveHelp(parseTree, builtinVariableRegistry, tokens, cursorOffset, localizer) { const activeFrame = findActiveCallFrame(tokens, cursorOffset); if (!activeFrame?.name) { return undefined; } const memberHelp = buildActiveMemberHelp(parseTree, builtinVariableRegistry, activeFrame, localizer); if (memberHelp) { return memberHelp; } const func = IfcExpressionFunctions_js_1.IfcExpressionFunctions.getFunction(activeFrame.name); const documentation = func?.getDocumentation(); if (!func || !documentation) { return undefined; } const activeArgument = func.getFormalArguments()[activeFrame.argumentIndex]; const label = func.getSignatureLabel(activeFrame.name); const fallback = `${label}: ${documentation.fallback}`; return { label, documentation: localizer ? localizer.t(documentation.key, fallback) : fallback, activeParameterIndex: activeFrame.argumentIndex, activeParameterLabel: activeArgument ? (0, Documentation_js_1.resolveLocalizedText)(activeArgument.displayLabel, localizer) ?? activeArgument.displayLabel?.fallback ?? activeArgument.name : undefined, activeParameterDocumentation: activeArgument ? (0, Documentation_js_1.resolveLocalizedText)(activeArgument.documentation, localizer) : undefined, }; } function toFunctionItem(name, localizer, label = name) { const insertText = toFunctionInsertText(label); return { kind: "builtinFunction", label, insertText, cursorOffset: insertText.length - 1, documentation: buildFunctionDocumentation(name, localizer, label), }; } function getPreferredMethodLabel(name) { return preferredMethodLabels.get(name) ?? name; } function isApplicablePrimitiveMethodFunction(name, receiverType) { if (unsupportedPrimitiveMethodFunctionNames.has(name)) { return false; } const func = IfcExpressionFunctions_js_1.IfcExpressionFunctions.getFunction(name); const firstArgument = func?.getFormalArguments()[0]; if (!func || !firstArgument) { return false; } const firstArgumentType = firstArgument.getType(); return (firstArgumentType.isAssignableFrom(receiverType) || firstArgumentType.overlapsWith(receiverType)); } function getPrimitiveMethodItems(receiverType, localizer) { return IfcExpressionFunctions_js_1.IfcExpressionFunctions.getBuiltinFunctionNames() .filter((name) => isApplicablePrimitiveMethodFunction(name, receiverType)) .map((name) => toFunctionItem(name, localizer, getPreferredMethodLabel(name))); } function toBuiltinRootItem(name, builtinVariableRegistry, localizer) { const normalizedName = name.startsWith("$") ? name : `${name}`; const definition = builtinVariableRegistry.getDefinition(normalizedName); if (!definition) { return undefined; } return { kind: "builtinRoot", label: toBuiltinLabel(definition.name), documentation: buildRootDocumentation(definition.name, definition.documentation, localizer), }; } function toEmptyExpressionStarterItem(name, builtinVariableRegistry, localizer) { if (name.startsWith("$")) { return toBuiltinRootItem(name, builtinVariableRegistry, localizer); } return IfcExpressionFunctions_js_1.IfcExpressionFunctions.getFunction(name) ? toFunctionItem(name, localizer) : undefined; } function getEmptyExpressionStarterItems(builtinVariableRegistry, localizer, starterNames = [ ...builtinVariableRegistry .getDefinitions() .map((definition) => toBuiltinLabel(definition.name)), ...emptyExpressionStarterFunctionNames, ]) { return starterNames .map((name) => toEmptyExpressionStarterItem(name, builtinVariableRegistry, localizer)) .filter((item) => item !== undefined); } function toMemberItem(definition, localizer) { if (definition.kind === "property") { return { kind: "builtinMemberProperty", label: definition.name, returnTypeName: toCompletionTypeName(definition.valueType), chainable: isChainableType(definition.valueType), documentation: buildMemberDocumentation(definition, localizer), }; } const insertText = toFunctionInsertText(definition.name); return { kind: "builtinMemberFunction", label: definition.name, insertText, cursorOffset: insertText.length - 1, argumentTypeNames: definition.argumentTypes.map((type) => type.getName()), returnTypeName: toCompletionTypeName(definition.returnType), chainable: isChainableType(definition.returnType), documentation: buildMemberDocumentation(definition, localizer), }; } function collectRuleCandidates(parser, caretTokenIndex, parseTree) { const completionCore = new antlr4_c3_1.CodeCompletionCore(parser); completionCore.preferredRules = new Set([ IfcExpressionParser_js_1.IfcExpressionParser.RULE_variableRef, IfcExpressionParser_js_1.IfcExpressionParser.RULE_methodAccessor, ]); return completionCore.collectCandidates(caretTokenIndex, parseTree); } function getMemberRuleCandidate(ruleCandidates) { return ruleCandidates.get(IfcExpressionParser_js_1.IfcExpressionParser.RULE_methodAccessor); } function hasBuiltinRootRuleCandidate(ruleCandidates) { return ruleCandidates.has(IfcExpressionParser_js_1.IfcExpressionParser.RULE_variableRef); } function hasIdentifierTokenCandidate(tokenCandidates) { return tokenCandidates.has(IfcExpressionParser_js_1.IfcExpressionParser.IDENTIFIER); } class IfcExpressionAutocomplete { static complete(input, cursorOffset, options = {}) { const builtinVariableRegistry = options.builtinVariableRegistry ?? BuiltinVariableRegistry_js_1.BuiltinVariableRegistry.getDefaultRegistry(); if (isEmptyExpressionInput(input)) { return { items: getEmptyExpressionStarterItems(builtinVariableRegistry, options.localizer, options.emptyExpressionStarters), replaceFrom: 0, replaceTo: input.length, }; } const parsed = parseAutocompleteInput(input, cursorOffset); const candidates = collectRuleCandidates(parsed.parser, parsed.caretTokenIndex, parsed.parseTree); const ruleCandidates = candidates.rules; const activeHelp = buildActiveHelp(parsed.parseTree, builtinVariableRegistry, parsed.tokens, cursorOffset, options.localizer); const memberCandidate = getMemberRuleCandidate(ruleCandidates); if (memberCandidate) { const range = findMemberReplaceRange(input, cursorOffset); if (!range) { return { items: [], replaceFrom: cursorOffset, replaceTo: cursorOffset, activeHelp, }; } const receiverType = findReceiverTypeForMemberSlot(parsed.parseTree, memberCandidate.startTokenIndex, builtinVariableRegistry); const typedPrefix = normalizeForMatch(input.slice(range.from, range.to)); const items = (receiverType instanceof ContextObjectType_js_1.ContextObjectType ? receiverType .getMemberDefinitions() .map((definition) => toMemberItem(definition, options.localizer)) : receiverType ? getPrimitiveMethodItems(receiverType, options.localizer) : []) .filter((item) => normalizeForMatch(item.label).startsWith(typedPrefix)) .sort((left, right) => left.label.localeCompare(right.label)); return { items, replaceFrom: range.from, replaceTo: range.to, activeHelp, }; } const functionRange = findFunctionReplaceRange(input, cursorOffset); if (functionRange && hasIdentifierTokenCandidate(candidates.tokens)) { const typedPrefix = normalizeForMatch(input.slice(functionRange.from, functionRange.to)); const items = IfcExpressionFunctions_js_1.IfcExpressionFunctions.getBuiltinFunctionNames() .map((name) => toFunctionItem(name, options.localizer)) .filter((item) => normalizeForMatch(item.label).startsWith(typedPrefix)) .sort((left, right) => left.label.localeCompare(right.label)); if (items.length > 0) { return { items, replaceFrom: functionRange.from, replaceTo: functionRange.to, activeHelp, }; } } const range = findBuiltinRootReplaceRange(input, cursorOffset); if (!range || !hasBuiltinRootRuleCandidate(ruleCandidates)) { return { items: [], replaceFrom: cursorOffset, replaceTo: cursorOffset, activeHelp, }; } const typedPrefix = normalizeForMatch(input.slice(range.from, range.to)); const items = builtinVariableRegistry .getDefinitions() .map((definition) => ({ kind: "builtinRoot", label: toBuiltinLabel(definition.name), documentation: buildRootDocumentation(definition.name, definition.documentation, options.localizer), })) .filter((item) => normalizeForMatch(item.label).startsWith(typedPrefix)) .sort((left, right) => left.label.localeCompare(right.label)); return { items, replaceFrom: range.from, replaceTo: range.to, activeHelp, }; } } exports.IfcExpressionAutocomplete = IfcExpressionAutocomplete; //# sourceMappingURL=IfcExpressionAutocomplete.js.map