blob: b879eee02ce53a9f7a377e56efe066b6b7b7dd54 [file]
/*---------------------------------------------------------------------------------------------
* 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 === '&gt;' && tv === '&gt;') ||
(pv === '&lt;' && (tv === '&lt;' || tv === '&gt;'))
) {
// 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,
}
}
}
}