| /*--------------------------------------------------------------------------------------------- |
| * Copyright (c) 2020 DeltaXML Ltd. All rights reserved. |
| * Licensed under the MIT License. See License.txt in the project root for license information. |
| *--------------------------------------------------------------------------------------------*/ |
| |
| import * as vscode from 'vscode' |
| import { |
| XslLexer, |
| GlobalInstructionData, |
| GlobalInstructionType, |
| } from './xslLexer' |
| import { |
| CharLevelState, |
| TokenLevelState, |
| BaseToken, |
| ErrorType, |
| Data, |
| XPathLexer, |
| } from './xpLexer' |
| import { FunctionData, XSLTnamespaces } from './functionData' |
| |
| export enum TagType { |
| XSLTstart, |
| XMLstart, |
| XSLTvar, |
| Start, |
| NonStart, |
| } |
| |
| export enum AttributeType { |
| None, |
| Variable, |
| VariableRef, |
| InstructionName, |
| InstructionMode, |
| UseAttributeSets, |
| ExcludeResultPrefixes, |
| XPath, |
| } |
| |
| export enum CurlyBraceType { |
| None, |
| Map, |
| Array, |
| } |
| |
| export interface XSLTToken extends BaseToken { |
| tagType?: TagType |
| } |
| |
| export interface ElementData { |
| variables: VariableData[] |
| currentVariable?: VariableData |
| xpathVariableCurrentlyBeingDefined?: boolean |
| identifierToken: XSLTToken |
| symbolName: string |
| symbolID: string |
| childSymbols: vscode.DocumentSymbol[] |
| namespacePrefixes: string[] |
| expectedChildElements: string[] |
| } |
| export interface XPathData { |
| token: BaseToken |
| variables: VariableData[] |
| preXPathVariable: boolean |
| xpathVariableCurrentlyBeingDefined: boolean |
| function?: BaseToken |
| functionArity?: number |
| isRangeVar?: boolean |
| awaitingMapKey?: boolean |
| curlyBraceType?: CurlyBraceType |
| } |
| |
| export interface VariableData { |
| token: BaseToken |
| name: string |
| } |
| |
| enum NameValidationError { |
| None, |
| NamespaceError, |
| NameError, |
| XSLTElementNameError, |
| XSLTAttributeNameError, |
| } |
| |
| export enum ValidationType { |
| XMLAttribute, |
| XMLElement, |
| XSLTAttribute, |
| PrefixedName, |
| Name, |
| } |
| |
| export enum DiagnosticCode { |
| none, |
| unresolvedVariableRef, |
| unresolvedGenericRef, |
| parseHtmlRef, |
| externalPrintRef, |
| fnWithNoContextItem, |
| currentWithNoContextItem, |
| groupOutsideForEachGroup, |
| groupOutsideMerge, |
| positionWithNoContextItem, |
| lastWithNoContextItem, |
| rootWithNoContextItem, |
| rootOnlyWithNoContextItem, |
| instrWithNoContextItem, |
| noContextItem, |
| regexNoContextItem, |
| } |
| |
| export class XsltTokenDiagnostics { |
| public static readonly xsltStartTokenNumber = |
| XslLexer.getXsltStartTokenNumber() |
| public static readonly xsltCatchVariables = [ |
| 'err:code', |
| 'err:description', |
| 'err:value', |
| 'err:module', |
| 'err:line-number', |
| 'err:column-number', |
| ] |
| public static readonly xslInclude = 'xsl:include' |
| public static readonly xslImport = 'xsl:import' |
| public static readonly xmlChars = ['lt', 'gt', 'quot', 'apos', 'amp'] |
| |
| public static readonly xslFunction = 'xsl:function' |
| |
| public static readonly xslNameAtt = 'name' |
| public static readonly xslModeAtt = 'mode' |
| public static readonly useAttSet = 'use-attribute-sets' |
| public static readonly xslUseAttSet = 'xsl:use-attribute-sets' |
| public static readonly excludePrefixes = 'exclude-result-prefixes' |
| public static readonly xslExcludePrefixes = 'xsl:exclude-result-prefixes' |
| public static readonly brackets = [ |
| CharLevelState.lB, |
| CharLevelState.lBr, |
| CharLevelState.lPr, |
| CharLevelState.rB, |
| CharLevelState.rBr, |
| CharLevelState.rPr, |
| ] |
| |
| public static isBracket(charState: CharLevelState) { |
| return XsltTokenDiagnostics.brackets.indexOf(charState) !== -1 |
| } |
| |
| private static nameStartCharRgx = new RegExp( |
| /[A-Z]|_|[a-z]|[\u00C0-\u00D6]|[\u00D8-\u00F6]|[\u00F8-\u02FF]|[\u0370-\u037D]|[\u037F-\u1FFF]|[\u200C-\u200D]|[\u2070-\u218F]|[\u2C00-\u2FEF]|[\u3001-\uD7FF]|[\uF900-\uFDCF]|[\uFDF0-\uFFFD]/ |
| ) |
| private static nameCharRgx = new RegExp( |
| /-|\.|[0-9]|\u00B7|[\u0300-\u036F]|[\u203F-\u2040]|[A-Z]|_|[a-z]|[\u00C0-\u00D6]|[\u00D8-\u00F6]|[\u00F8-\u02FF]|[\u0370-\u037D]|[\u037F-\u1FFF]|[\u200C-\u200D]|[\u2070-\u218F]|[\u2C00-\u2FEF]|[\u3001-\uD7FF]|[\uF900-\uFDCF]|[\uFDF0-\uFFFD]/ |
| ) |
| |
| private static validateName( |
| name: string, |
| type: ValidationType, |
| xmlnsPrefixes: string[], |
| elementStack?: ElementData[], |
| expectedAttributes?: string[] |
| ): NameValidationError { |
| let valid = NameValidationError.None |
| if (name.trim().length === 0) { |
| return NameValidationError.NameError |
| } |
| if ( |
| type === ValidationType.XMLAttribute || |
| type === ValidationType.XSLTAttribute |
| ) { |
| if ( |
| name === 'xml:space' || |
| name === 'xml:lang' || |
| name === 'xml:base' || |
| name === 'xml:id' |
| ) { |
| return NameValidationError.None |
| } |
| } |
| let nameParts = name.split(':') |
| if (nameParts.length > 2) { |
| return NameValidationError.NameError |
| } else { |
| if (nameParts.length === 2) { |
| let prefix = nameParts[0] |
| if (type === ValidationType.XMLElement) { |
| // TODO: when within literal result element, iterate up stack until we get to an XSLT instruction: |
| const expectedNames: string[] = |
| elementStack && elementStack.length > 0 |
| ? elementStack[elementStack.length - 1].expectedChildElements |
| : ['xsl:transform', 'xsl:stylesheet', 'xsl:package'] |
| if (prefix === 'xsl' || prefix === 'ixsl') { |
| if (expectedNames.length === 0 && elementStack) { |
| const withinNextIteration = |
| elementStack[elementStack.length - 1].symbolName === |
| 'xsl:next-iteration' |
| valid = |
| name === 'xsl:with-param' && withinNextIteration |
| ? NameValidationError.None |
| : NameValidationError.XSLTElementNameError |
| } else { |
| valid = |
| expectedNames.indexOf(name) > -1 |
| ? NameValidationError.None |
| : NameValidationError.XSLTElementNameError |
| if ( |
| valid !== NameValidationError.None && |
| (name === 'xsl:next-iteration' || name === 'xsl:break') |
| ) { |
| const withinIterarator = elementStack?.find( |
| (item) => item.symbolName === 'xsl:iterate' |
| ) |
| if (withinIterarator) { |
| valid = NameValidationError.None |
| } |
| } |
| } |
| return valid |
| } else { |
| valid = |
| xmlnsPrefixes.indexOf(prefix) > -1 |
| ? NameValidationError.None |
| : NameValidationError.NamespaceError |
| } |
| } else if (prefix === 'xsl' && type === ValidationType.XSLTAttribute) { |
| // TODO: for attributes on non-xsl instructions, check that name is in the attributeGroup: xsl:literal-result-element-attributes (e.g. xsl:expand-text) |
| //valid = xmlnsPrefixes.indexOf(prefix) > -1? NameValidationError.None: NameValidationError.NamespaceError; |
| } else { |
| valid = |
| xmlnsPrefixes.indexOf(prefix) > -1 |
| ? NameValidationError.None |
| : NameValidationError.NamespaceError |
| } |
| } else if ( |
| (type === ValidationType.XSLTAttribute || |
| type === ValidationType.XMLAttribute) && |
| expectedAttributes |
| ) { |
| valid = |
| expectedAttributes.indexOf(name) > -1 |
| ? NameValidationError.None |
| : NameValidationError.XSLTAttributeNameError |
| return valid |
| } |
| if (valid === NameValidationError.None) { |
| nameParts.forEach((namePart) => { |
| if (valid === NameValidationError.None) { |
| let charsOK = true |
| let firstChar = true |
| let charExists = false |
| for (let s of namePart) { |
| if (firstChar) { |
| firstChar = false |
| charExists = true |
| charsOK = XsltTokenDiagnostics.nameStartCharRgx.test(s) |
| if (!charsOK) { |
| break |
| } |
| } else { |
| charsOK = XsltTokenDiagnostics.nameCharRgx.test(s) |
| if (!charsOK) { |
| break |
| } |
| } |
| } |
| valid = |
| charExists && charsOK |
| ? NameValidationError.None |
| : NameValidationError.NameError |
| } |
| }) |
| } |
| } |
| return valid |
| } |
| |
| public static validateSimpleName(name: string) { |
| let valid = false |
| const nameParts = name.split(':') |
| if (nameParts.length > 2) { |
| return false |
| } |
| nameParts.forEach((namePart) => { |
| let charsOK = true |
| let firstChar = true |
| let charExists = false |
| for (let s of namePart) { |
| if (firstChar) { |
| firstChar = false |
| charExists = true |
| charsOK = XsltTokenDiagnostics.nameStartCharRgx.test(s) |
| if (!charsOK) { |
| break |
| } |
| } else { |
| charsOK = XsltTokenDiagnostics.nameCharRgx.test(s) |
| if (!charsOK) { |
| break |
| } |
| } |
| } |
| valid = charExists && charsOK |
| }) |
| return valid |
| } |
| |
| public static calculateDiagnostics = ( |
| document: vscode.TextDocument, |
| allTokens: BaseToken[], |
| globalInstructionData: GlobalInstructionData[], |
| importedInstructionData: GlobalInstructionData[], |
| symbols: vscode.DocumentSymbol[], |
| globalVariables: string[], |
| xmlNamespaces: string[] |
| ): vscode.Diagnostic[] => { |
| let inScopeVariablesList: VariableData[] = [] |
| let xpathVariableCurrentlyBeingDefined: boolean |
| let elementStack: ElementData[] = [] |
| let inScopeXPathVariablesList: VariableData[] = [] |
| let anonymousFunctionParamList: VariableData[] = [] |
| let xpathStack: XPathData[] = [] |
| let tagType = TagType.NonStart |
| let preXPathVariable = false |
| let anonymousFunctionParams = false |
| let xsltVariableDeclarations: BaseToken[] = [] |
| let unresolvedXsltVariableReferences: BaseToken[] = [] |
| let prevToken: BaseToken | null = null |
| let includeOrImport = false |
| let problemTokens: BaseToken[] = [] |
| let topLevelSymbols: vscode.DocumentSymbol[] = symbols |
| let tagIdentifierName: string = '' |
| let lastTokenIndex = allTokens.length - 1 |
| let inheritedPrefixes: string[] = xmlNamespaces |
| let globalVariableData: VariableData[] = [] |
| let checkedGlobalVarNames: string[] = [] |
| let checkedGlobalFnNames: string[] = [] |
| let importedGlobalVarNames: string[] = globalVariables |
| let importedGlobalFnNames: string[] = [] |
| let incrementFunctionArity = false |
| let xsltPrefixesToURIs = new Map<string, XSLTnamespaces>() |
| let dtdStarted = false |
| let dtdEnded = false |
| let namedTemplates: Map<string, string[]> = new Map() |
| let globalModes: string[] = ['#current', '#default'] |
| let globalKeys: string[] = [] |
| let globalAccumulatorNames: string[] = [] |
| let globalAttributeSetNames: string[] = [] |
| let ifThenStack: BaseToken[] = [] |
| if (dtdStarted) { |
| } |
| |
| globalInstructionData.forEach((instruction) => { |
| switch (instruction.type) { |
| case GlobalInstructionType.Variable: |
| case GlobalInstructionType.Parameter: |
| if (checkedGlobalVarNames.indexOf(instruction.name) < 0) { |
| checkedGlobalVarNames.push(instruction.name) |
| } else { |
| instruction.token['error'] = ErrorType.DuplicateVarName |
| instruction.token.value = instruction.name |
| problemTokens.push(instruction.token) |
| } |
| globalVariableData.push({ |
| token: instruction.token, |
| name: instruction.name, |
| }) |
| xsltVariableDeclarations.push(instruction.token) |
| break |
| case GlobalInstructionType.Function: |
| let functionNameWithArity = |
| instruction.name + '#' + instruction.idNumber |
| if (checkedGlobalFnNames.indexOf(functionNameWithArity) < 0) { |
| checkedGlobalFnNames.push(functionNameWithArity) |
| } else { |
| instruction.token['error'] = ErrorType.DuplicateFnName |
| instruction.token.value = functionNameWithArity |
| problemTokens.push(instruction.token) |
| } |
| break |
| case GlobalInstructionType.Template: |
| if (namedTemplates.get(instruction.name)) { |
| instruction.token['error'] = ErrorType.DuplicateTemplateName |
| instruction.token.value = instruction.name |
| problemTokens.push(instruction.token) |
| } else { |
| let members = instruction.memberNames ? instruction.memberNames : [] |
| namedTemplates.set(instruction.name, members) |
| } |
| break |
| case GlobalInstructionType.Mode: |
| let modes = instruction.name.split(/\s+/) |
| globalModes = globalModes.concat(modes) |
| break |
| case GlobalInstructionType.Key: |
| globalKeys.push(instruction.name) |
| break |
| case GlobalInstructionType.Accumulator: |
| if (globalAccumulatorNames.indexOf(instruction.name) < 0) { |
| globalAccumulatorNames.push(instruction.name) |
| } else { |
| instruction.token['error'] = ErrorType.DuplicateAccumulatorName |
| instruction.token.value = instruction.name |
| problemTokens.push(instruction.token) |
| } |
| break |
| case GlobalInstructionType.AttributeSet: |
| globalAttributeSetNames.push(instruction.name) |
| break |
| case GlobalInstructionType.RootXMLNS: |
| inheritedPrefixes.push(instruction.name) |
| break |
| } |
| }) |
| |
| importedInstructionData.forEach((instruction) => { |
| switch (instruction.type) { |
| case GlobalInstructionType.Variable: |
| case GlobalInstructionType.Parameter: |
| if (checkedGlobalVarNames.indexOf(instruction.name) < 0) { |
| checkedGlobalVarNames.push(instruction.name) |
| importedGlobalVarNames.push(instruction.name) |
| } |
| break |
| case GlobalInstructionType.Function: |
| let functionNameWithArity = |
| instruction.name + '#' + instruction.idNumber |
| if (checkedGlobalFnNames.indexOf(functionNameWithArity) < 0) { |
| checkedGlobalFnNames.push(functionNameWithArity) |
| importedGlobalFnNames.push(functionNameWithArity) |
| } |
| break |
| case GlobalInstructionType.Template: |
| let members = instruction.memberNames ? instruction.memberNames : [] |
| namedTemplates.set(instruction.name, members) |
| break |
| case GlobalInstructionType.Mode: |
| let modes = instruction.name.split(/\s+/) |
| globalModes = globalModes.concat(modes) |
| break |
| case GlobalInstructionType.Key: |
| globalKeys.push(instruction.name) |
| break |
| case GlobalInstructionType.Accumulator: |
| globalAccumulatorNames.push(instruction.name) |
| break |
| case GlobalInstructionType.AttributeSet: |
| globalAttributeSetNames.push(instruction.name) |
| break |
| } |
| }) |
| |
| xsltPrefixesToURIs.set('array', XSLTnamespaces.Array) |
| xsltPrefixesToURIs.set('map', XSLTnamespaces.Map) |
| xsltPrefixesToURIs.set('math', XSLTnamespaces.Map) |
| xsltPrefixesToURIs.set('xs', XSLTnamespaces.XMLSchema) |
| xsltPrefixesToURIs.set('fn', XSLTnamespaces.XPath) |
| xsltPrefixesToURIs.set('xsl', XSLTnamespaces.XSLT) |
| xsltPrefixesToURIs.set('ixsl', XSLTnamespaces.IXSL) |
| xsltPrefixesToURIs.set('dfdl', XSLTnamespaces.dfdl) |
| inheritedPrefixes = inheritedPrefixes.concat([ |
| 'array', |
| 'map', |
| 'math', |
| 'xs', |
| 'fn', |
| 'xsl', |
| 'ixsl', |
| 'dfdl', |
| ]) |
| |
| allTokens.forEach((token, index) => { |
| let xpathCharType = <CharLevelState>token.charType |
| let xpathTokenType = <TokenLevelState>token.tokenType |
| if (xpathStack.length > 0) { |
| const tv = xpathStack[xpathStack.length - 1].token.value |
| if ( |
| prevToken?.charType === CharLevelState.sep && |
| prevToken.value === ',' && |
| (tv === 'for' || tv === 'let' || tv === 'every') |
| ) { |
| if (xpathTokenType !== TokenLevelState.variable) { |
| token['error'] = ErrorType.ExpectedDollarAfterComma |
| problemTokens.push(token) |
| } |
| } |
| } |
| |
| switch (xpathTokenType) { |
| case TokenLevelState.string: |
| if (token.error) { |
| problemTokens.push(token) |
| } |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens |
| ) |
| if (xpathStack.length > 0) { |
| let xp = xpathStack[xpathStack.length - 1] |
| if ( |
| xp.functionArity === 0 && |
| (xp.function?.value === 'key' || |
| xp.function?.value.startsWith('accumulator-')) |
| ) { |
| let keyVal = token.value.substring(1, token.value.length - 1) |
| if (xp.function.value === 'key') { |
| if (globalKeys.indexOf(keyVal) < 0) { |
| token['error'] = ErrorType.XSLTKeyUnresolved |
| problemTokens.push(token) |
| } |
| } else if (globalAccumulatorNames.indexOf(keyVal) < 0) { |
| token['error'] = ErrorType.AccumulatorNameUnresolved |
| problemTokens.push(token) |
| } |
| } |
| } |
| break |
| case TokenLevelState.axisName: |
| if (token.error) { |
| problemTokens.push(token) |
| } |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens |
| ) |
| break |
| case TokenLevelState.variable: |
| if ( |
| (preXPathVariable && !xpathVariableCurrentlyBeingDefined) || |
| anonymousFunctionParams |
| ) { |
| let fullVariableName = token.value |
| let currentVariable = { |
| token: token, |
| name: fullVariableName.substring(1), |
| } |
| if (anonymousFunctionParams) { |
| anonymousFunctionParamList.push(currentVariable) |
| xsltVariableDeclarations.push(token) |
| } else { |
| inScopeXPathVariablesList.push(currentVariable) |
| xpathVariableCurrentlyBeingDefined = true |
| xsltVariableDeclarations.push(token) |
| } |
| } else { |
| let prefixEnd = token.value.indexOf(':') |
| if (prefixEnd !== -1) { |
| let prefix = token.value.substring(1, prefixEnd) |
| if (inheritedPrefixes.indexOf(prefix) === -1) { |
| token['error'] = ErrorType.XPathPrefix |
| problemTokens.push(token) |
| } |
| } |
| // don't include any current pending variable declarations when resolving |
| let globalVarName: string | null = null |
| if (tagType === TagType.XSLTvar && elementStack.length === 1) { |
| globalVarName = tagIdentifierName |
| } |
| let unResolvedToken = |
| XsltTokenDiagnostics.resolveXPathVariableReference( |
| globalVarName, |
| document, |
| importedGlobalVarNames, |
| token, |
| xpathVariableCurrentlyBeingDefined, |
| inScopeXPathVariablesList, |
| xpathStack, |
| inScopeVariablesList, |
| elementStack, |
| xmlNamespaces |
| ) |
| if (unResolvedToken !== null) { |
| unresolvedXsltVariableReferences.push(unResolvedToken) |
| } |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens |
| ) |
| } |
| break |
| case TokenLevelState.complexExpression: |
| let valueText = token.value |
| switch (valueText) { |
| case 'if': |
| ifThenStack.push(token) |
| break |
| case 'every': |
| case 'for': |
| case 'let': |
| case 'some': |
| if (allTokens.length > index + 2) { |
| const opToken = allTokens[index + 2] |
| const expectedOp = valueText === 'let' ? ':=' : 'in' |
| if (opToken.value !== expectedOp) { |
| opToken['error'] = ErrorType.XPathExpectedComplex |
| problemTokens.push(opToken) |
| } |
| } |
| if (index > 0) { |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| allTokens[index - 1], |
| problemTokens, |
| TokenLevelState.Unset |
| ) |
| } |
| preXPathVariable = true |
| xpathVariableCurrentlyBeingDefined = false |
| xpathStack.push({ |
| token: token, |
| variables: inScopeXPathVariablesList.slice(), |
| preXPathVariable: preXPathVariable, |
| xpathVariableCurrentlyBeingDefined: |
| xpathVariableCurrentlyBeingDefined, |
| isRangeVar: true, |
| }) |
| break |
| case 'then': |
| if (ifThenStack.length > 0) { |
| ifThenStack.pop() |
| } else { |
| token.error = ErrorType.XPathUnexpected |
| problemTokens.push(token) |
| } |
| xpathStack.push({ |
| token: token, |
| variables: inScopeXPathVariablesList.slice(), |
| preXPathVariable: preXPathVariable, |
| xpathVariableCurrentlyBeingDefined: |
| xpathVariableCurrentlyBeingDefined, |
| }) |
| inScopeXPathVariablesList = [] |
| break |
| case 'return': |
| case 'satisfies': |
| case 'else': |
| let tokenValBeforeDelete = |
| xpathStack.length > 0 |
| ? xpathStack[xpathStack.length - 1].token.value |
| : '' |
| if (xpathStack.length > 1) { |
| let deleteCount = 0 |
| for (let i = xpathStack.length - 1; i > -1; i--) { |
| const sv = xpathStack[i].token.value |
| if (sv === 'return' || sv === 'else' || sv === 'satisfies') { |
| deleteCount++ |
| } else { |
| break |
| } |
| } |
| if (deleteCount > 0) { |
| xpathStack.splice(xpathStack.length - deleteCount) |
| } |
| } |
| |
| if (xpathStack.length > 0) { |
| let peekedStack = xpathStack[xpathStack.length - 1] |
| if (peekedStack) { |
| if (valueText === 'else') { |
| preXPathVariable = peekedStack.preXPathVariable |
| } else { |
| // todo: if after a return AND a ',' prePathVariable = true; see $pos := $c. |
| preXPathVariable = false |
| } |
| xpathVariableCurrentlyBeingDefined = |
| peekedStack.xpathVariableCurrentlyBeingDefined |
| let matchingToken: string = |
| XsltTokenDiagnostics.getMatchingToken( |
| peekedStack.token.value |
| ) |
| if (!token.error && matchingToken !== token.value) { |
| token['error'] = ErrorType.BracketNesting |
| problemTokens.push(token) |
| } else { |
| peekedStack.token = token |
| } |
| } else { |
| inScopeXPathVariablesList = [] |
| preXPathVariable = false |
| xpathVariableCurrentlyBeingDefined = false |
| } |
| } else if (tokenValBeforeDelete !== '') { |
| let matchingToken: string = |
| XsltTokenDiagnostics.getMatchingToken(tokenValBeforeDelete) |
| if (!token.error && matchingToken !== token.value) { |
| token['error'] = ErrorType.BracketNesting |
| problemTokens.push(token) |
| } |
| } |
| break |
| } |
| break |
| case TokenLevelState.mapKey: |
| if ( |
| !( |
| prevToken && |
| prevToken.tokenType === TokenLevelState.operator && |
| (prevToken.value === ',' || prevToken.value === '{') |
| ) |
| ) { |
| token['error'] = ErrorType.XPathUnexpected |
| problemTokens.push(token) |
| } |
| break |
| case TokenLevelState.operator: |
| let isXPathError = false |
| let tv = token.value |
| |
| // start checks |
| let stackItem: XPathData | undefined = |
| xpathStack.length > 0 |
| ? xpathStack[xpathStack.length - 1] |
| : undefined |
| const sv = stackItem?.token.value |
| const tokenIsComma = tv === ',' |
| const popStackLaterForComma = |
| sv && |
| tokenIsComma && |
| (sv === 'return' || sv === 'else' || sv === 'satisfies') |
| if (popStackLaterForComma && xpathStack.length > 1) { |
| stackItem = xpathStack[xpathStack.length - 2] |
| } |
| if (stackItem && stackItem.curlyBraceType === CurlyBraceType.Map) { |
| if (tokenIsComma) { |
| if (stackItem.awaitingMapKey) { |
| isXPathError = true |
| } else { |
| stackItem.awaitingMapKey = true |
| } |
| } else if (tv === '}' && stackItem.awaitingMapKey) { |
| isXPathError = true |
| } |
| } |
| if (prevToken?.tokenType === TokenLevelState.complexExpression) { |
| let currCharType = <CharLevelState>token.charType |
| if ( |
| currCharType === CharLevelState.rB || |
| currCharType === CharLevelState.rBr || |
| currCharType === CharLevelState.rPr |
| ) { |
| if ( |
| prevToken.value === 'return' || |
| prevToken.value === 'satisfies' || |
| prevToken.value === 'else' || |
| prevToken.value === 'then' |
| ) { |
| prevToken['error'] = ErrorType.XPathAwaiting |
| problemTokens.push(prevToken) |
| } |
| } else if (tokenIsComma) { |
| prevToken['error'] = ErrorType.XPathAwaiting |
| problemTokens.push(prevToken) |
| } |
| } else if (prevToken?.tokenType === TokenLevelState.uriLiteral) { |
| token['error'] = ErrorType.XPathUnexpected |
| problemTokens.push(token) |
| } else if ( |
| prevToken && |
| tv !== '/' && |
| prevToken.value !== '/' && |
| !prevToken.error |
| ) { |
| let currCharType = <CharLevelState>token.charType |
| let nextToken = |
| index + 1 < allTokens.length ? allTokens[index + 1] : undefined |
| if (tv === ':') { |
| if ( |
| stackItem && |
| stackItem.curlyBraceType === CurlyBraceType.Map |
| ) { |
| if (stackItem.awaitingMapKey) { |
| stackItem.awaitingMapKey = false |
| } else { |
| isXPathError = true |
| } |
| } else if ( |
| prevToken.tokenType === TokenLevelState.nodeNameTest || |
| prevToken.tokenType === TokenLevelState.attributeNameTest |
| ) { |
| isXPathError = !( |
| prevToken.startCharacter + prevToken.length === |
| token.startCharacter && nextToken?.value === '*' |
| ) |
| } else { |
| isXPathError = true |
| } |
| } |
| if (tv === 'map' || tv === 'array') { |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens, |
| TokenLevelState.function |
| ) |
| } else if ( |
| (tv === '+' || tv === '-') && |
| nextToken && |
| nextToken.tokenType !== TokenLevelState.string |
| ) { |
| // either a number of an operator so show no error |
| } else if (prevToken.tokenType === TokenLevelState.operator) { |
| // current type is operator and previous type is operator |
| let prevCharType = <CharLevelState>prevToken.charType |
| let pv = prevToken.value |
| |
| switch (currCharType) { |
| case CharLevelState.rB: |
| case CharLevelState.rBr: |
| case CharLevelState.rPr: |
| if (!XsltTokenDiagnostics.isBracket(prevCharType)) { |
| // +) is not ok but )) or ( ) is ok |
| if ( |
| !( |
| (prevToken.charType === CharLevelState.sep && |
| pv === '?' && |
| tv === ')') || |
| (prevToken.charType === CharLevelState.dSep && |
| (pv === '{}' || pv === '()' || pv === '[]')) |
| ) |
| ) { |
| isXPathError = true |
| } |
| } |
| break |
| case CharLevelState.dSep: |
| if ( |
| prevCharType === CharLevelState.rB || |
| prevCharType === CharLevelState.rPr || |
| prevCharType === CharLevelState.rBr || |
| (prevCharType === CharLevelState.dSep && |
| (pv === '()' || pv === '[]' || pv === '{}')) |
| ) { |
| // allow: ) != |
| isXPathError = tv === '*:' |
| } else if (tv === '*:' || tv === '//') { |
| // no error |
| } else if ( |
| !( |
| (tv === '{}' && (pv === 'map' || pv === 'array')) || |
| tv === '()' || |
| tv === '[]' |
| ) |
| ) { |
| isXPathError = true |
| } |
| break |
| case CharLevelState.lB: |
| case CharLevelState.lBr: |
| case CharLevelState.lPr: |
| // +( is ok |
| break |
| default: |
| switch (prevCharType) { |
| case CharLevelState.rB: |
| case CharLevelState.rBr: |
| case CharLevelState.rPr: |
| // ), or )+ are ok |
| break |
| case CharLevelState.dSep: |
| if (!(pv === '()' || pv === '{}' || pv === '[]')) { |
| isXPathError = true |
| } |
| break |
| default: |
| // (+ or ++ are not ok |
| if ( |
| (pv === '>' && tv === '>') || |
| (pv === '<' && (tv === '<' || tv === '>')) |
| ) { |
| // allow << <> or >> |
| } else if (tv === 'as') { |
| isXPathError = |
| pv !== 'castable' && pv !== 'cast' && pv !== 'treat' |
| } else if (tv === 'of') { |
| isXPathError = pv !== 'instance' |
| } else if ( |
| !( |
| (pv === '?' && (tv === ',' || tv === ')')) || |
| (tv === '?' && |
| (pv === '(' || pv === ')' || pv === ',')) || |
| (pv === '!' && tv === '?') || |
| (pv === '[' && tv === '?') |
| ) |
| ) { |
| isXPathError = true |
| } |
| break |
| } |
| } |
| } |
| if (isXPathError) { |
| token['error'] = ErrorType.XPathUnexpected |
| problemTokens.push(token) |
| // token is pushed onto problemTokens later |
| } |
| } |
| // end checks |
| let functionToken: BaseToken | null = null |
| switch (xpathCharType) { |
| case CharLevelState.lBr: |
| let curlyBraceType = CurlyBraceType.None |
| if ( |
| prevToken && |
| prevToken.tokenType === TokenLevelState.operator |
| ) { |
| if (prevToken.value === 'map') { |
| curlyBraceType = CurlyBraceType.Map |
| } else if (prevToken.value === 'array') { |
| curlyBraceType = CurlyBraceType.Array |
| } |
| } |
| const stackItem: XPathData = { |
| token: token, |
| variables: inScopeXPathVariablesList, |
| preXPathVariable: preXPathVariable, |
| xpathVariableCurrentlyBeingDefined: |
| xpathVariableCurrentlyBeingDefined, |
| curlyBraceType, |
| } |
| if (curlyBraceType === CurlyBraceType.Map) { |
| stackItem.awaitingMapKey = true |
| } |
| xpathStack.push(stackItem) |
| if (anonymousFunctionParams) { |
| // handle case: function($a) {$a + 8} pass params to inside '{...}' |
| inScopeXPathVariablesList = anonymousFunctionParamList |
| anonymousFunctionParamList = [] |
| anonymousFunctionParams = false |
| } else { |
| inScopeXPathVariablesList = [] |
| } |
| preXPathVariable = false |
| xpathVariableCurrentlyBeingDefined = false |
| break |
| case CharLevelState.lB: |
| // handle case: function($a) |
| if ( |
| !anonymousFunctionParams && |
| prevToken?.tokenType !== TokenLevelState.nodeType |
| ) { |
| anonymousFunctionParams = |
| prevToken?.tokenType === TokenLevelState.anonymousFunction |
| } |
| if (prevToken?.tokenType === TokenLevelState.function) { |
| functionToken = prevToken |
| } else if (prevToken?.tokenType === TokenLevelState.variable) { |
| // TODO: check arity of variables of type 'function' |
| incrementFunctionArity = false |
| } |
| // intentionally no-break; |
| case CharLevelState.lPr: |
| let xpathItem: XPathData = { |
| token: token, |
| variables: inScopeXPathVariablesList, |
| preXPathVariable: preXPathVariable, |
| xpathVariableCurrentlyBeingDefined: |
| xpathVariableCurrentlyBeingDefined, |
| } |
| if (functionToken) { |
| xpathItem.function = functionToken |
| if (incrementFunctionArity) { |
| xpathItem.functionArity = 1 |
| incrementFunctionArity = false |
| } else { |
| xpathItem.functionArity = 0 |
| } |
| } |
| xpathStack.push(xpathItem) |
| preXPathVariable = false |
| inScopeXPathVariablesList = [] |
| xpathVariableCurrentlyBeingDefined = false |
| break |
| case CharLevelState.rB: |
| case CharLevelState.rPr: |
| case CharLevelState.rBr: |
| if (xpathStack.length > 1) { |
| let deleteCount = 0 |
| for (let i = xpathStack.length - 1; i > -1; i--) { |
| const sv = xpathStack[i].token.value |
| if (sv === 'return' || sv === 'else' || sv === 'satisfies') { |
| deleteCount++ |
| } else { |
| break |
| } |
| } |
| if (deleteCount > 0) { |
| xpathStack.splice(xpathStack.length - deleteCount) |
| } |
| } |
| |
| if (xpathStack.length > 0) { |
| let poppedData = xpathStack.pop() |
| if (poppedData) { |
| if (poppedData.token.value === 'then') { |
| poppedData.token['error'] = ErrorType.BracketNesting |
| problemTokens.push(poppedData.token) |
| } |
| inScopeXPathVariablesList = poppedData.variables |
| preXPathVariable = poppedData.preXPathVariable |
| xpathVariableCurrentlyBeingDefined = |
| poppedData.xpathVariableCurrentlyBeingDefined |
| if ( |
| poppedData.function && |
| poppedData.functionArity !== undefined |
| ) { |
| if (prevToken?.charType !== CharLevelState.lB) { |
| if (poppedData.functionArity !== undefined) { |
| poppedData.functionArity++ |
| } |
| } |
| let { isValid, qFunctionName, fErrorType } = |
| XsltTokenDiagnostics.isValidFunctionName( |
| inheritedPrefixes, |
| xsltPrefixesToURIs, |
| poppedData.function, |
| checkedGlobalFnNames, |
| poppedData.functionArity |
| ) |
| if (!isValid) { |
| poppedData.function['error'] = fErrorType |
| poppedData.function['value'] = qFunctionName |
| problemTokens.push(poppedData.function) |
| } |
| } |
| } else { |
| inScopeXPathVariablesList = [] |
| preXPathVariable = false |
| xpathVariableCurrentlyBeingDefined = false |
| } |
| } |
| if (token.error && !isXPathError) { |
| // any error should already have been added by lexer: |
| problemTokens.push(token) |
| } |
| break |
| case CharLevelState.sep: |
| if (token.value === ',') { |
| if (xpathStack.length > 0) { |
| let xp = xpathStack[xpathStack.length - 1] |
| if (xp.functionArity !== undefined) { |
| xp.functionArity++ |
| } |
| if (xp.isRangeVar) { |
| preXPathVariable = xp.preXPathVariable |
| } |
| let nonBracketedThen = -1 |
| for (let i = xpathStack.length - 1; i > -1; i--) { |
| const xpathItem = xpathStack[i].token |
| const val = xpathItem.value |
| if ( |
| !( |
| val === 'return' || |
| val === 'else' || |
| val === 'satisfies' || |
| val === 'then' |
| ) |
| ) { |
| break |
| } else if (val === 'then') { |
| nonBracketedThen = i |
| } |
| } |
| if (nonBracketedThen > -1) { |
| //xpathStack.splice(nonBracketedThen, 1); |
| token['error'] = ErrorType.ExpectedElseAfterThen |
| problemTokens.push(token) |
| } |
| const sv = xp.token.value |
| if (sv === 'return' || sv === 'else' || sv === 'satisfies') { |
| let poppedData = xpathStack.pop() |
| if (poppedData) { |
| inScopeXPathVariablesList = poppedData.variables |
| if (sv === 'else') { |
| preXPathVariable = poppedData.preXPathVariable |
| } else { |
| // todo: if after a return AND a ',' prePathVariable = true; see $pos := $c. |
| preXPathVariable = false |
| } |
| xpathVariableCurrentlyBeingDefined = false |
| } |
| } |
| } |
| xpathVariableCurrentlyBeingDefined = false |
| } |
| break |
| case CharLevelState.dSep: |
| const isEmptyBracketsToken = token.value === '()' |
| if ( |
| isEmptyBracketsToken && |
| prevToken?.tokenType === TokenLevelState.function |
| ) { |
| const fnArity = incrementFunctionArity ? 1 : 0 |
| incrementFunctionArity = false |
| let { isValid, qFunctionName, fErrorType } = |
| XsltTokenDiagnostics.isValidFunctionName( |
| inheritedPrefixes, |
| xsltPrefixesToURIs, |
| prevToken, |
| checkedGlobalFnNames, |
| fnArity |
| ) |
| if (!isValid) { |
| prevToken['error'] = fErrorType |
| prevToken['value'] = qFunctionName |
| problemTokens.push(prevToken) |
| prevToken.tokenType = TokenLevelState.Unset |
| } |
| } else if ( |
| isEmptyBracketsToken && |
| prevToken?.tokenType === TokenLevelState.variable |
| ) { |
| // TODO: check arity of variable of type 'function' |
| incrementFunctionArity = false |
| } else if (token.value === '=>') { |
| incrementFunctionArity = true |
| } |
| break |
| } |
| break |
| case TokenLevelState.nodeType: |
| if (token.value === ':*' && prevToken && !prevToken.error) { |
| let pfx = |
| prevToken.tokenType === TokenLevelState.attributeNameTest |
| ? prevToken.value.substring(1) |
| : prevToken.value |
| if (inheritedPrefixes.indexOf(pfx) === -1 && pfx !== 'xml') { |
| prevToken['error'] = ErrorType.XPathPrefix |
| problemTokens.push(prevToken) |
| } |
| } |
| break |
| case TokenLevelState.attributeNameTest: |
| case TokenLevelState.nodeNameTest: |
| if (token.error) { |
| problemTokens.push(token) |
| } else { |
| let tokenValue |
| let validationType |
| let skipValidation = false |
| if (xpathTokenType === TokenLevelState.nodeNameTest) { |
| tokenValue = token.value |
| validationType = ValidationType.PrefixedName |
| } else { |
| tokenValue = token.value.substr(1) |
| validationType = ValidationType.XMLAttribute |
| skipValidation = token.value === '@xml' |
| if (!skipValidation && token.value === '@') { |
| let nextToken = |
| allTokens.length > index + 1 ? allTokens[index + 1] : null |
| skipValidation = nextToken |
| ? token.value === '@' && |
| (nextToken.value === '*' || nextToken.value === '*:') |
| : false |
| } |
| } |
| if (!skipValidation) { |
| let validateResult = XsltTokenDiagnostics.validateName( |
| tokenValue, |
| validationType, |
| inheritedPrefixes |
| ) |
| if (validateResult !== NameValidationError.None) { |
| token['error'] = |
| validateResult === NameValidationError.NameError |
| ? ErrorType.XPathName |
| : ErrorType.XPathPrefix |
| token['value'] = token.value |
| problemTokens.push(token) |
| } |
| } |
| } |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens |
| ) |
| break |
| case TokenLevelState.functionNameTest: |
| let { isValid, qFunctionName, fErrorType } = |
| XsltTokenDiagnostics.isValidFunctionName( |
| inheritedPrefixes, |
| xsltPrefixesToURIs, |
| token, |
| checkedGlobalFnNames |
| ) |
| if (!isValid) { |
| token['error'] = fErrorType |
| token['value'] = qFunctionName |
| problemTokens.push(token) |
| } |
| break |
| case TokenLevelState.function: |
| case TokenLevelState.number: |
| XsltTokenDiagnostics.checkTokenIsExpected( |
| prevToken, |
| token, |
| problemTokens |
| ) |
| break |
| case TokenLevelState.simpleType: |
| let tValue = token.value |
| let tParts = tValue.split(':') |
| let isValidType = false |
| if (tValue === '*' || tValue === '?' || tValue === '+') { |
| // e.g. xs:integer* don't check name |
| isValidType = true |
| } else if (tParts.length === 1) { |
| let nextToken = |
| allTokens.length > index + 1 ? allTokens[index + 1] : null |
| |
| if ( |
| nextToken && |
| (nextToken.charType === CharLevelState.lB || |
| (nextToken.charType === CharLevelState.dSep && |
| nextToken.value === '()')) |
| ) { |
| isValidType = Data.nodeTypes.indexOf(tParts[0]) > -1 |
| if (!isValidType) { |
| isValidType = Data.nonFunctionTypes.indexOf(tParts[0]) > -1 |
| } |
| } |
| } else if (tParts.length === 2) { |
| let nsType = xsltPrefixesToURIs.get(tParts[0]) |
| if (nsType !== undefined) { |
| if (nsType === XSLTnamespaces.XMLSchema) { |
| if (tParts[1] === 'numeric') { |
| isValidType = true |
| } else { |
| isValidType = |
| FunctionData.schema.indexOf(tParts[1] + '#1') > -1 |
| } |
| } |
| } |
| } |
| if (!isValidType) { |
| token['error'] = ErrorType.XPathTypeName |
| problemTokens.push(token) |
| } |
| break |
| case TokenLevelState.entityRef: |
| if (token.error) { |
| problemTokens.push(token) |
| } else { |
| let validationResult, entityName |
| ;({ validationResult, entityName } = |
| XsltTokenDiagnostics.validateEntityRef( |
| token.value, |
| dtdEnded, |
| inheritedPrefixes |
| )) |
| if (validationResult !== NameValidationError.None) { |
| token['error'] = ErrorType.EntityName |
| token['value'] = entityName |
| problemTokens.push(token) |
| } |
| } |
| break |
| } |
| if (index === lastTokenIndex && !token.error) { |
| if (token.tokenType === TokenLevelState.operator) { |
| XsltTokenDiagnostics.checkFinalXPathToken( |
| token, |
| allTokens, |
| index, |
| problemTokens |
| ) |
| } |
| if (xpathStack.length > 0 && !token.error) { |
| let disallowedStackItem: BaseToken | undefined |
| for (let index = xpathStack.length - 1; index > -1; index--) { |
| const trailingToken = xpathStack[index].token |
| const tv = trailingToken.value |
| const allowedToken = |
| tv === 'return' || tv === 'else' || tv === 'satisfies' |
| if (!allowedToken) { |
| disallowedStackItem = trailingToken |
| break |
| } |
| } |
| if (disallowedStackItem) { |
| disallowedStackItem['error'] = ErrorType.BracketNesting |
| problemTokens.push(disallowedStackItem) |
| } |
| } |
| if (token.tokenType === TokenLevelState.string && !token.error) { |
| XPathLexer.checkStringLiteralEnd(token) |
| if (token.error) { |
| problemTokens.push(token) |
| } |
| } |
| } |
| prevToken = |
| token.tokenType === TokenLevelState.comment ? prevToken : token |
| if (index === lastTokenIndex) { |
| // xml is not well-nested if items still on the stack at the end |
| // but report errors and try to keep some part of the tree: |
| if (token.tokenType === TokenLevelState.complexExpression) { |
| token['error'] = ErrorType.XPathAwaiting |
| problemTokens.push(token) |
| } |
| if (elementStack.length > 0) { |
| let usedtoken = false |
| while (elementStack.length > 0) { |
| let poppedData = elementStack.pop() |
| let endToken: BaseToken |
| if (poppedData) { |
| if (usedtoken) { |
| // use final token as we don't know what the end token is |
| // but reduce lendth by one on each iteration - so its well nested |
| endToken = token |
| endToken.length = endToken.length - 1 |
| } else { |
| endToken = token |
| usedtoken = true |
| } |
| let errorToken = Object.assign({}, poppedData.identifierToken) |
| errorToken['error'] = ErrorType.ElementNesting |
| problemTokens.push(errorToken) |
| let symbol = XsltTokenDiagnostics.createSymbolFromElementTokens( |
| poppedData.symbolName, |
| poppedData.symbolID, |
| poppedData.identifierToken, |
| endToken |
| ) |
| if (symbol !== null) { |
| if (elementStack.length > 0) { |
| elementStack[elementStack.length - 1].childSymbols.push( |
| symbol |
| ) |
| } else { |
| topLevelSymbols.push(symbol) |
| } |
| } |
| } |
| } |
| } |
| } |
| }) |
| let variableRefDiagnostics = |
| XsltTokenDiagnostics.getDiagnosticsFromUnusedVariableTokens( |
| xsltVariableDeclarations, |
| unresolvedXsltVariableReferences, |
| includeOrImport |
| ) |
| let allDiagnostics = |
| XsltTokenDiagnostics.appendDiagnosticsFromProblemTokens( |
| variableRefDiagnostics, |
| problemTokens |
| ) |
| return allDiagnostics |
| } |
| |
| public static checkFinalXPathToken( |
| prevToken: BaseToken, |
| allTokens: BaseToken[], |
| index: number, |
| problemTokens: BaseToken[] |
| ) { |
| let isValid = false |
| switch (prevToken.charType) { |
| case CharLevelState.rB: |
| case CharLevelState.rBr: |
| case CharLevelState.rPr: |
| isValid = true |
| break |
| case CharLevelState.dSep: |
| isValid = |
| prevToken.value === '()' || |
| prevToken.value === '[]' || |
| prevToken.value === '{}' |
| break |
| default: |
| if (prevToken.value === '%') { |
| isValid = true |
| } else if (prevToken.value === '/' || prevToken.value === '.') { |
| // these are ok provided that the previous token was XSLT or previous token was ,; |
| let prevToken2 = allTokens[index - 2] |
| let tokenBeforePrevWasXSLT = |
| prevToken2.tokenType >= XsltTokenDiagnostics.xsltStartTokenNumber |
| isValid = |
| tokenBeforePrevWasXSLT || |
| (prevToken2.tokenType === TokenLevelState.operator && |
| prevToken2.charType !== CharLevelState.rB && |
| prevToken2.charType !== CharLevelState.rBr && |
| prevToken2.charType !== CharLevelState.rPr) |
| } |
| break |
| } |
| if (!isValid) { |
| prevToken['error'] = ErrorType.XPathOperatorUnexpected |
| problemTokens.push(prevToken) |
| } |
| } |
| |
| private static validateEntityRef( |
| entityName: string, |
| dtdEnded: boolean, |
| inheritedPrefixes: string[] |
| ) { |
| let validationResult = NameValidationError.None |
| if (entityName.length > 2 && entityName.endsWith(';')) { |
| entityName = entityName.substring(1, entityName.length - 1) |
| if (entityName.length > 1 && entityName.charAt(0) === '#') { |
| let validNumber |
| if (entityName.charAt(1).toLocaleLowerCase() === 'x') { |
| validNumber = /^#[Xx][0-9a-fA-F]+$/.test(entityName) |
| } else { |
| validNumber = /^#[0-9]+$/.test(entityName) |
| } |
| validationResult = validNumber |
| ? NameValidationError.None |
| : NameValidationError.NameError |
| } else if (!dtdEnded) { |
| let isXmlChar = XsltTokenDiagnostics.xmlChars.indexOf(entityName) > -1 |
| validationResult = isXmlChar |
| ? NameValidationError.None |
| : NameValidationError.NameError |
| } else { |
| validationResult = XsltTokenDiagnostics.validateName( |
| entityName, |
| ValidationType.Name, |
| inheritedPrefixes |
| ) |
| } |
| } else { |
| validationResult = NameValidationError.NameError |
| } |
| return { validationResult, entityName } |
| } |
| |
| private static checkTokenIsExpected( |
| prevToken: BaseToken | null, |
| token: BaseToken, |
| problemTokens: BaseToken[], |
| overridType?: TokenLevelState |
| ) { |
| if (token.error) { |
| return |
| } |
| let tokenType = overridType ? overridType : token.tokenType |
| let errorSingleSeparators: string[] |
| if (tokenType === TokenLevelState.number) { |
| errorSingleSeparators = ['|'] |
| } else if (tokenType === TokenLevelState.string) { |
| errorSingleSeparators = ['|', '+', '-', '*'] |
| } else { |
| errorSingleSeparators = [] |
| } |
| let errDoubleSeparators |
| if (tokenType === TokenLevelState.nodeNameTest) { |
| errDoubleSeparators = ['{}', '[]', '()'] |
| } else if ( |
| tokenType === TokenLevelState.number || |
| tokenType === TokenLevelState.string |
| ) { |
| errDoubleSeparators = ['{}', '[]', '()', '*:', '::', '//'] |
| } else { |
| errDoubleSeparators = ['{}', '[]', '()', '*:', '::'] |
| } |
| if (prevToken) { |
| let isXPathError = false |
| if ( |
| prevToken.tokenType === TokenLevelState.complexExpression || |
| prevToken.tokenType === TokenLevelState.entityRef |
| ) { |
| // no error |
| } else if ( |
| prevToken.tokenType === TokenLevelState.uriLiteral && |
| tokenType !== TokenLevelState.nodeNameTest |
| ) { |
| isXPathError = true |
| } else if (prevToken.tokenType === TokenLevelState.operator) { |
| if ( |
| prevToken.charType === CharLevelState.rB || |
| prevToken.charType === CharLevelState.rPr || |
| prevToken.charType === CharLevelState.rPr |
| ) { |
| isXPathError = true |
| } else if (prevToken.charType === CharLevelState.dSep) { |
| if (errDoubleSeparators.indexOf(prevToken.value) !== -1) { |
| isXPathError = true |
| } |
| } else if (prevToken.charType === CharLevelState.sep) { |
| if (errorSingleSeparators.indexOf(prevToken.value) !== -1) { |
| isXPathError = true |
| } |
| } |
| } else if ( |
| tokenType === TokenLevelState.nodeNameTest && |
| prevToken.tokenType === TokenLevelState.uriLiteral |
| ) { |
| // no error |
| } else if ( |
| tokenType === TokenLevelState.string && |
| prevToken.tokenType === TokenLevelState.string |
| ) { |
| const currentTokenFirstChar = token.value.charAt(0) |
| if (currentTokenFirstChar === '"' || currentTokenFirstChar === "'") { |
| isXPathError = true |
| } |
| } else { |
| isXPathError = true |
| } |
| if (isXPathError) { |
| let errType: ErrorType = |
| tokenType === TokenLevelState.function |
| ? ErrorType.XPathFunctionUnexpected |
| : ErrorType.XPathUnexpected |
| token.error = errType |
| problemTokens.push(token) |
| } |
| } |
| } |
| |
| public static isValidFunctionName( |
| xmlnsPrefixes: string[], |
| xmlnsData: Map<string, XSLTnamespaces>, |
| token: BaseToken, |
| checkedGlobalFnNames: string[], |
| arity?: number |
| ) { |
| let tokenValue |
| if (arity === undefined) { |
| let parts = token.value.split('#') |
| arity = Number.parseInt(parts[1]) |
| tokenValue = parts[0] |
| } else { |
| tokenValue = token.value |
| } |
| let qFunctionName = tokenValue + '#' + arity |
| let fNameParts = qFunctionName.split(':') |
| let isValid = false |
| let fErrorType = ErrorType.XPathFunction |
| if (fNameParts.length === 1) { |
| if (tokenValue === 'concat' || tokenValue === 'codepoints-to-string') { |
| isValid = arity > 0 |
| } else { |
| isValid = FunctionData.xpath.indexOf(fNameParts[0]) > -1 |
| } |
| } else { |
| let xsltType = xmlnsData.get(fNameParts[0]) |
| if (xmlnsPrefixes.indexOf(fNameParts[0]) < 0) { |
| // prefix is not declared |
| fErrorType = ErrorType.XPathFunctionNamespace |
| isValid = false |
| } else if ( |
| xsltType === XSLTnamespaces.NotDefined || |
| xsltType === undefined |
| ) { |
| isValid = checkedGlobalFnNames.indexOf(qFunctionName) > -1 |
| } else { |
| switch (xsltType) { |
| case XSLTnamespaces.XPath: |
| isValid = FunctionData.xpath.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.Array: |
| isValid = FunctionData.array.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.Map: |
| isValid = FunctionData.map.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.Math: |
| isValid = FunctionData.math.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.XMLSchema: |
| isValid = FunctionData.schema.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.IXSL: |
| isValid = FunctionData.ixsl.indexOf(fNameParts[1]) > -1 |
| break |
| case XSLTnamespaces.Saxon: |
| case XSLTnamespaces.ExpathArchive: |
| case XSLTnamespaces.ExpathBinary: |
| case XSLTnamespaces.ExpathFile: |
| case XSLTnamespaces.Exslt: |
| case XSLTnamespaces.ExsltMath: |
| case XSLTnamespaces.ExsltRegex: |
| case XSLTnamespaces.ExsltSets: |
| case XSLTnamespaces.ExsltStrings: |
| isValid = true |
| break |
| case XSLTnamespaces.dfdl: |
| isValid = true |
| break |
| } |
| } |
| } |
| fErrorType = isValid ? ErrorType.None : fErrorType |
| return { isValid, qFunctionName, fErrorType } |
| } |
| |
| public static getTextForToken( |
| lineNumber: number, |
| token: BaseToken, |
| document: vscode.TextDocument |
| ) { |
| let start = token.startCharacter |
| if (start < 0) { |
| console.error( |
| 'ERROR: Found illegal token for document: ' + document.fileName |
| ) |
| console.error( |
| 'token.startCharacter less than zero: ' + token.startCharacter |
| ) |
| console.error(token) |
| start = 0 |
| } |
| |
| let startPos = new vscode.Position(lineNumber, start) |
| let endPos = new vscode.Position(lineNumber, start + token.length) |
| const currentLine = document.lineAt(lineNumber) |
| let valueRange = currentLine.range.with(startPos, endPos) |
| let valueText = document.getText(valueRange) |
| return valueText |
| } |
| |
| public static resolveXPathVariableReference( |
| globalVarName: string | null, |
| document: vscode.TextDocument, |
| importedVariables: string[], |
| token: BaseToken, |
| xpathVariableCurrentlyBeingDefined: boolean, |
| inScopeXPathVariablesList: VariableData[], |
| xpathStack: XPathData[], |
| inScopeVariablesList: VariableData[], |
| elementStack: ElementData[], |
| xmlNamespaces: string[] |
| ): BaseToken | null { |
| let fullVarName = XsltTokenDiagnostics.getTextForToken( |
| token.line, |
| token, |
| document |
| ) |
| let varName = fullVarName.startsWith('$') |
| ? fullVarName.substring(1) |
| : fullVarName.substring(1, fullVarName.length - 1) |
| let result: BaseToken | null = null |
| let globalVariable = null |
| |
| let resolved = this.resolveVariableName( |
| inScopeXPathVariablesList, |
| varName, |
| xpathVariableCurrentlyBeingDefined, |
| globalVariable |
| ) |
| if (!resolved) { |
| resolved = this.resolveStackVariableName(xpathStack, varName) |
| } |
| if (!resolved) { |
| resolved = this.resolveVariableName( |
| inScopeVariablesList, |
| varName, |
| false, |
| globalVariable |
| ) |
| } |
| if (!resolved) { |
| if (elementStack.length === 1 && globalVarName === varName) { |
| // don't asign |
| } else { |
| resolved = this.resolveStackVariableName(elementStack, varName) |
| } |
| } |
| let importedResolved = false |
| if (!resolved) { |
| importedResolved = |
| globalVarName !== varName && importedVariables.indexOf(varName) > -1 |
| } |
| // Parse the namespaces from the DFDL document that contains the XPath segments. If the variable is |
| // defined in an external namespace that we are importing, don't flag it. This is a temporary solution |
| // that we can remove once we have the ability to create a DFDL model outside of a Parse operation. |
| if (!resolved && !importedResolved && varName.includes(':')) { |
| let splits = varName.split(':') |
| resolved = xmlNamespaces.includes(splits[0]) ? token : undefined |
| } |
| if (!resolved && !importedResolved) { |
| result = token |
| } |
| return result |
| } |
| |
| private static createSymbolFromElementTokens( |
| name: string, |
| id: string, |
| fullStartToken: XSLTToken, |
| fullEndToken: BaseToken, |
| innerToken?: BaseToken |
| ) { |
| // innerToken to be used if its an attribute-value for example |
| let kind: vscode.SymbolKind |
| if (name.trim().length === 0) { |
| return null |
| } |
| switch (fullStartToken.tagType) { |
| case TagType.XSLTvar: |
| kind = vscode.SymbolKind.Enum |
| break |
| case TagType.XSLTstart: |
| switch (name) { |
| case 'xsl:package': |
| case 'xsl:stylesheet': |
| case 'xsl:transform': |
| kind = vscode.SymbolKind.Package |
| break |
| case 'xsl:function': |
| kind = vscode.SymbolKind.Function |
| break |
| case 'xsl:template': |
| kind = vscode.SymbolKind.Interface |
| break |
| case 'xsl:if': |
| case 'xsl:when': |
| case 'xsl:otherwise': |
| kind = vscode.SymbolKind.Namespace |
| break |
| case 'xsl:key': |
| kind = vscode.SymbolKind.Key |
| break |
| case 'xsl:sequence': |
| kind = vscode.SymbolKind.Module |
| break |
| case 'xsl:value-of': |
| case 'xsl:text': |
| kind = vscode.SymbolKind.String |
| break |
| case 'xsl:for-each': |
| case 'xsl:for-each-group': |
| case 'xsl:apply-templates': |
| case 'xsl:iterate': |
| kind = vscode.SymbolKind.EnumMember |
| break |
| case 'xsl:import': |
| case 'xsl:include': |
| kind = vscode.SymbolKind.File |
| break |
| case 'xsl:choose': |
| kind = vscode.SymbolKind.TypeParameter |
| break |
| default: |
| kind = vscode.SymbolKind.Object |
| break |
| } |
| break |
| case TagType.XMLstart: |
| kind = vscode.SymbolKind.Object |
| break |
| default: |
| kind = vscode.SymbolKind.Null |
| break |
| } |
| let startCharPos = |
| fullStartToken.startCharacter > 0 ? fullStartToken.startCharacter - 1 : 0 |
| let startPos = new vscode.Position(fullStartToken.line, startCharPos) |
| let endPos = new vscode.Position( |
| fullEndToken.line, |
| fullEndToken.startCharacter + fullEndToken.length + 1 |
| ) |
| let innerStartPos |
| let innerEndPos |
| if (innerToken) { |
| innerStartPos = new vscode.Position( |
| innerToken.line, |
| innerToken.startCharacter |
| ) |
| innerEndPos = new vscode.Position( |
| innerToken.line, |
| innerToken.startCharacter + innerToken.length |
| ) |
| } else { |
| innerStartPos = new vscode.Position( |
| fullStartToken.line, |
| fullStartToken.startCharacter |
| ) |
| innerEndPos = new vscode.Position( |
| fullEndToken.line, |
| fullStartToken.startCharacter + fullStartToken.length |
| ) |
| } |
| let fullRange = new vscode.Range(startPos, endPos) |
| let innerRange = new vscode.Range(innerStartPos, innerEndPos) |
| // check for error! |
| if (!fullRange.contains(innerRange)) { |
| innerStartPos = new vscode.Position( |
| fullStartToken.line, |
| fullStartToken.startCharacter |
| ) |
| innerEndPos = new vscode.Position( |
| fullStartToken.line, |
| fullStartToken.startCharacter + fullStartToken.length |
| ) |
| innerRange = new vscode.Range(innerStartPos, innerEndPos) |
| } |
| let detail = '' |
| let fullSymbolName = id.length > 0 ? name + ' \u203A ' + id : name |
| |
| if (fullRange.contains(innerRange)) { |
| return new vscode.DocumentSymbol( |
| fullSymbolName, |
| detail, |
| kind, |
| fullRange, |
| innerRange |
| ) |
| } else { |
| return null |
| } |
| } |
| |
| public static resolveVariableName( |
| variableList: VariableData[], |
| varName: string, |
| xpathVariableCurrentlyBeingDefined: boolean, |
| globalXsltVariable: VariableData | null |
| ) { |
| let resolved = false |
| if (resolved) { |
| } |
| let decrementedLength = variableList.length - 1 |
| let globalVariableName = globalXsltVariable?.name |
| let defnData: VariableData | undefined = undefined |
| // last items in list of declared parameters must be resolved first: |
| for (let i = decrementedLength; i > -1; i--) { |
| let data = variableList[i] |
| if (xpathVariableCurrentlyBeingDefined && i === decrementedLength) { |
| // do nothing: we skip last item in list as it's currently being defined |
| } else if (data.name === varName && globalVariableName !== data.name) { |
| defnData = data |
| data.token['referenced'] = true |
| break |
| } |
| } |
| return defnData?.token |
| } |
| |
| public static resolveStackVariableName( |
| elementStack: ElementData[] | XPathData[], |
| varName: string |
| ) { |
| let resolvedDefnToken: BaseToken | undefined |
| let globalXsltVariable: VariableData | null = null |
| |
| for (let i = elementStack.length - 1; i > -1; i--) { |
| let inheritedVariables = elementStack[i].variables |
| let xpathBeingDefinedInit = |
| elementStack[i].xpathVariableCurrentlyBeingDefined |
| let xpathBeingDefined = !( |
| xpathBeingDefinedInit === undefined || xpathBeingDefinedInit === false |
| ) |
| if (i === 1) { |
| // at the level of a global variable declaration |
| let elementData: ElementData = <ElementData>elementStack[i] |
| let currentVar = elementData.currentVariable |
| if (currentVar) { |
| // must be inside a global variable declaration - keep this: |
| globalXsltVariable = currentVar |
| } |
| } |
| resolvedDefnToken = this.resolveVariableName( |
| inheritedVariables, |
| varName, |
| xpathBeingDefined, |
| globalXsltVariable |
| ) |
| if (resolvedDefnToken) { |
| break |
| } |
| } |
| return resolvedDefnToken |
| } |
| |
| private static getDiagnosticsFromUnusedVariableTokens( |
| unusedVariableTokens: BaseToken[], |
| unresolvedVariableTokens: BaseToken[], |
| includeOrImport: boolean |
| ): vscode.Diagnostic[] { |
| let result: any[] = [] |
| for (let token of unusedVariableTokens) { |
| if (token.referenced === undefined) { |
| result.push(this.createUnusedVarDiagnostic(token)) |
| } |
| } |
| for (let token of unresolvedVariableTokens) { |
| result.push(this.createUnresolvedVarDiagnostic(token, includeOrImport)) |
| } |
| return result |
| } |
| |
| private static appendDiagnosticsFromProblemTokens( |
| variableRefDiagnostics: vscode.Diagnostic[], |
| tokens: BaseToken[] |
| ): vscode.Diagnostic[] { |
| tokens.forEach((token) => { |
| let line = token.line |
| let endChar = token.startCharacter + token.length |
| let tokenValue = token.value |
| let msg: string |
| let errCode = DiagnosticCode.none |
| let diagnosticMetadata: vscode.DiagnosticTag[] = [] |
| let severity = vscode.DiagnosticSeverity.Error |
| switch (token.error) { |
| case ErrorType.AxisName: |
| msg = `XPath: Invalid axis name: '${tokenValue}` |
| break |
| case ErrorType.BracketNesting: |
| let matchingChar: any = |
| XsltTokenDiagnostics.getMatchingSymbol(tokenValue) |
| msg = |
| matchingChar.length === 0 |
| ? `XPath: No match found for '${tokenValue}'` |
| : `'${tokenValue}' has no matching '${matchingChar}'` |
| diagnosticMetadata = [vscode.DiagnosticTag.Unnecessary] |
| break |
| case ErrorType.ElementNesting: |
| msg = `XML: Start tag '${tokenValue}' has no matching close tag` |
| break |
| case ErrorType.ExpectedElseAfterThen: |
| msg = `XML: Expected 'else' but found '${tokenValue}'` |
| break |
| case ErrorType.ExpectedDollarAfterComma: |
| msg = `XML: Expected '$' but found '${tokenValue}'` |
| break |
| case ErrorType.EntityName: |
| msg = `XML: Invalid entity name '${tokenValue}'` |
| break |
| case ErrorType.XPathKeyword: |
| msg = `XPath: Found: '${tokenValue}' expected keyword or operator` |
| break |
| case ErrorType.DuplicateParameterName: |
| msg = `XSLT: Duplicate parameter name: '${tokenValue}'` |
| break |
| case ErrorType.XSLTKeyUnresolved: |
| msg = `XSLT: xsl:key declaration with name '${tokenValue}' not found` |
| break |
| case ErrorType.AccumulatorNameUnresolved: |
| msg = `XSLT: xsl:accumulator with name '${tokenValue}' not found` |
| break |
| case ErrorType.XPathUnexpected: |
| msg = `XPath: Expression context - unexpected token here: ${tokenValue} ` |
| break |
| case ErrorType.XPathFunctionUnexpected: |
| msg = `XPath: Unexpected function after expression: '${tokenValue}()' ` |
| break |
| case ErrorType.XPathName: |
| msg = `XPath: Invalid name: '${tokenValue}'` |
| break |
| case ErrorType.XPathOperatorUnexpected: |
| msg = `XPath: Operator unexpected at this position: '${tokenValue}'` |
| break |
| case ErrorType.XPathAwaiting: |
| msg = `XPath: Expected expression following: '${tokenValue}'` |
| break |
| case ErrorType.XPathStringLiteral: |
| msg = `String literal not terminated properly: ${tokenValue}` |
| break |
| case ErrorType.XPathFunction: |
| let parts = tokenValue.split('#') |
| msg = `XPath: Function: '${parts[0]}' with ${parts[1]} arguments not found` |
| break |
| case ErrorType.XPathTypeName: |
| msg = `XPath: Invalid type: '${tokenValue}'` |
| break |
| case ErrorType.XPathFunctionNamespace: |
| let partsNs = tokenValue.split('#') |
| msg = `XPath: Undeclared prefix in function: '${partsNs[0]}'` |
| break |
| case ErrorType.XPathExpectedComplex: |
| const expected = tokenValue === ':=' ? 'in' : ':=' |
| msg = `XPath: '${tokenValue}' is invalid here, expected '${expected}'` |
| break |
| case ErrorType.XPathPrefix: |
| msg = `XPath: Undeclared prefix in name: '${tokenValue}'` |
| break |
| case ErrorType.DuplicateVarName: |
| msg = `XSLT: Duplicate global variable/parameter name: '${tokenValue}'` |
| break |
| case ErrorType.DuplicateFnName: |
| msg = `XSLT: Duplicate function name and arity: '${tokenValue}'` |
| break |
| case ErrorType.DuplicateTemplateName: |
| msg = `XSLT: Duplicate xsl:template name '${tokenValue}'` |
| break |
| case ErrorType.DuplicateAccumulatorName: |
| msg = `XSLT: Duplicate xsl:accumulator name '${tokenValue}'` |
| break |
| default: |
| msg = 'Unexepected Error' |
| break |
| } |
| if (token.startCharacter > -1 && endChar > -1) { |
| variableRefDiagnostics.push({ |
| code: errCode, |
| message: msg, |
| range: new vscode.Range( |
| new vscode.Position(line, token.startCharacter), |
| new vscode.Position(line, endChar) |
| ), |
| severity: severity, |
| tags: diagnosticMetadata, |
| source: '', |
| }) |
| } |
| }) |
| return variableRefDiagnostics |
| } |
| |
| private static getMatchingSymbol(text: string) { |
| let r = '' |
| switch (text) { |
| case '(': |
| r = ')' |
| break |
| case '[': |
| r = ']' |
| break |
| case '{': |
| r = '}' |
| break |
| case ')': |
| r = '(' |
| break |
| case ']': |
| r = '[' |
| break |
| case '}': |
| r = '{' |
| break |
| case 'if': |
| r = 'then' |
| break |
| case 'then': |
| r = 'else' |
| break |
| case 'let': |
| case 'for': |
| r = 'return' |
| break |
| case 'every': |
| case 'some': |
| r = 'satisfies' |
| break |
| case 'else': |
| r = 'then' |
| break |
| default: |
| r = '' |
| break |
| } |
| return r |
| } |
| |
| private static getMatchingToken(text: string) { |
| let r = '' |
| switch (text) { |
| case 'let': |
| case 'for': |
| r = 'return' |
| break |
| case 'every': |
| case 'some': |
| r = 'satisfies' |
| break |
| case 'then': |
| r = 'else' |
| } |
| return r |
| } |
| |
| private static createUnusedVarDiagnostic( |
| token: BaseToken |
| ): vscode.Diagnostic { |
| let line = token.line |
| let endChar = token.startCharacter + token.length |
| return { |
| code: '', |
| message: 'variable is unused', |
| range: new vscode.Range( |
| new vscode.Position(line, token.startCharacter), |
| new vscode.Position(line, endChar) |
| ), |
| severity: vscode.DiagnosticSeverity.Hint, |
| tags: [vscode.DiagnosticTag.Unnecessary], |
| source: '', |
| } |
| } |
| |
| private static createUnresolvedVarDiagnostic( |
| token: BaseToken, |
| includeOrImport: boolean |
| ): vscode.Diagnostic { |
| let line = token.line |
| let endChar = token.startCharacter + token.length |
| if (includeOrImport) { |
| return { |
| code: '', |
| message: `XPath: The variable/parameter: ${token.value} cannot be resolved here, but it may be defined in an external module.`, |
| range: new vscode.Range( |
| new vscode.Position(line, token.startCharacter), |
| new vscode.Position(line, endChar) |
| ), |
| severity: vscode.DiagnosticSeverity.Warning, |
| } |
| } else { |
| return { |
| code: '', |
| message: `XPath: The variable/parameter ${token.value} cannot be resolved`, |
| range: new vscode.Range( |
| new vscode.Position(line, token.startCharacter), |
| new vscode.Position(line, endChar) |
| ), |
| severity: vscode.DiagnosticSeverity.Error, |
| } |
| } |
| } |
| } |