A linter for XSL/XSLT stylesheets. It first checks that every stylesheet is well-formed and every XPath expression compiles, then flags stylistic, semantic, and logical mistakes — each pinpointed to its exact line and column, with a fix where the correction is unambiguous. Think of it as the checks ESLint gives JavaScript, but for XSL.
npx @maxonfjvipon/xslint path/to/stylesheets
It also runs in your editor — VS Code and compatible editors and JetBrains IDEs — in CI via the GitHub Action, or embedded through the language server.
[ERROR] sheet.xsl(2:1) The xsl:output instruction is missing. (not-using-output)
[WARNING] sheet.xsl(3:3) The match attribute starts with //, which scans the whole tree. (starts-with-double-slash)
[WARNING] sheet.xsl(4:5) A variable has a single-character name. (short-names)
Pointed at core stylesheets from the three most widely-used XSLT projects
— DocBook-XSL
(1.0), TEI (2.0), and
DITA-OT (1.0/2.0), 70 files
in all — xslint surfaced 1,974 findings across 22 different checks, with
no false positives from its validators: 106 xsl:choose blocks
with no xsl:otherwise, 67 unused named templates, 40 stylesheet
functions never called, and more. Real stylistic and logical findings in code
that has shipped for decades.
71 checks, each with its rationale. A run reports the
33 marked
recommended unless asked for
--preset all: what a processor refuses, and the dead code whose
report is almost never wrong.
| Check | Type | Severity | Description |
|---|---|---|---|
| blank-nested-if | xpath | warning | An xsl:if holds nothing but another xsl:if, stating one condition in two places. Combine the tests with 'and' in one xsl:if, unless the outer test guards the inner one. |
| duplicate-param-name | xpath | error recommended | Two xsl:param siblings share a @name. Each parameter of a template, function, or stylesheet must have a distinct name. |
| duplicate-with-param-name | xpath | error recommended | A single call passes two xsl:with-param with the same @name. Each parameter can be supplied only once. |
| empty-choose | xpath | error recommended | An xsl:choose has no xsl:when branch. Add one, or drop the xsl:choose and keep its content directly. |
| empty-content-in-instructions | xpath | warning recommended | An instruction element such as xsl:for-each or xsl:if has no content. Add content or remove the empty element. |
| empty-variable | xpath | warning | An empty xsl:variable binds the empty string without saying so. State the value with select="''" or give it some content. |
| function-complexity | xpath | warning | The function contains more than 50 XSLT elements. Split it into smaller functions. |
| function-template-is-not-child-of-stylesheet | xpath | error recommended | An xsl:function or xsl:template is nested inside another element. Move it up to the top level, as a child of the stylesheet element. |
| function-use-in-xslt-1 | xpath | error recommended | An xsl:function requires XSLT 2.0 or later, and the version in force here is below that. Use a named template, or raise that version to 2.0. |
| incorrect-use-of-boolean-constants | xpath | warning suggested fix recommended | The test is the string 'true' or 'false', a non-empty string that is always true, not a boolean. Use true() or false() instead. |
| malformed-version-in-stylesheet | xpath | error recommended | A declared version is not a decimal number, so it names no XSLT version and every check gated on one stays quiet. Declare it as a decimal, such as 2.0. |
| missing-id-in-stylesheet | xpath | warning | The stylesheet element has no @id attribute. Declare it to specify the unique identifier explicitly. |
| missing-or-empty-href | xpath | error recommended | The @href is missing or empty, so this pulls in no module and no processor will load the stylesheet. Name the module to pull in. |
| missing-or-empty-name | xpath | error recommended | The @name is missing or empty, so it holds no QName and no processor will load this stylesheet. Give it a name. |
| missing-version-in-stylesheet | xpath | warning suggested fix | The version attribute is missing on the stylesheet's root element. Declare it to specify the stylesheet version explicitly. |
| mode-or-priority-without-match | xpath | error suggested fix recommended | An xsl:template that has no match attribute must have no mode attribute and no priority attribute. Add match attribute or remove mode or priority. |
| modern-construct-in-xslt-1 | xpath | error recommended | This XSLT 2.0 construct stands where the version in force is below 2.0, which a conformant 1.0 processor rejects. Use a 1.0 equivalent, or raise that version to 2.0. |
| name-starts-with-numeric | xpath | error recommended | A variable, parameter, function, or template name starts with a digit, so no processor loads this stylesheet. Rename it to start with a letter. |
| not-creating-attribute-correctly | xpath | warning | xsl:attribute with a static name on a literal element can be a literal attribute (an AVT for a computed value). Write it inline. |
| not-using-attribute-test-in-if-when-node | xpath | error recommended | An xsl:if or xsl:when has no @test attribute. Add one, or remove the element. |
| otherwise-not-last | xpath | error recommended | An xsl:otherwise must be the last child of its xsl:choose. Move it after every xsl:when. |
| oversized-template | xpath | warning | The template contains more than 100 XSLT elements. Decompose it with apply-templates into focused templates. |
| param-after-content | xpath | error recommended | An xsl:param must be declared before any other content in its xsl:template or xsl:function. Move it above the content. |
| setting-value-of-variable-incorrectly | xpath | warning | A variable or parameter is assigned via a nested xsl:value-of instead of the select attribute. Use select syntax instead. |
| short-names | xpath | warning | A variable, parameter, function, or template has a single-character name. Use a descriptive name that reveals intent. |
| sort-not-first | xpath | error recommended | An xsl:sort must come before any other content in its xsl:for-each or xsl:apply-templates. Move it to the front. |
| stylesheet-has-no-templates | xpath | warning | The stylesheet declares nothing at all, so it contributes nothing to a transformation. Add a declaration to it. |
| template-has-no-name-or-match | xpath | error recommended | An xsl:template must have a @match or @name attribute, or both. Add one of them. |
| text-outside-xsl-text | xpath | warning suggested fix | Literal text appears directly inside an XSLT instruction element. Wrap it in xsl:text. |
| too-many-templates | xpath | warning | The stylesheet declares 10 or more templates, too many to read in one file. Split it into imported modules. |
| use-choose-without-otherwise | xpath | warning | xsl:choose has no xsl:otherwise branch. Add xsl:otherwise to handle unmatched cases explicitly. |
| use-single-option-for-choose | xpath | warning | xsl:choose contains only one xsl:when branch. Replace it with xsl:if. |
| using-disable-output-escaping | xpath | warning suggested fix | disable-output-escaping='yes' is set, which produces implementation-defined output. Use xsl:copy-of or literal elements instead. |
| using-not-outermost-stylesheet | xpath | error recommended | An xsl:stylesheet, xsl:transform or xsl:package is nested inside another element. It must be the outermost element of the document. |
| variable-or-param-with-select-and-content | xpath | error suggested fix recommended | An xsl:variable, xsl:param or xsl:with-param has both a select attribute and a non-empty body. Use only one way to set the value. |
| when-or-otherwise-outside-choose | xpath | error recommended | An xsl:when or xsl:otherwise is valid only as a child of xsl:choose. Move it into an xsl:choose, or use xsl:if. |
| with-param-use-in-invalid-parent-node | xpath | error recommended | xsl:with-param stands under an element taking no parameters at the version in force, which a processor of that version refuses. Move it into a call, or raise the version. |
| with-param-without-name | xpath | error recommended | xsl:with-param requires a @name attribute to bind the parameter it passes. Add one. |
| unreachable-function | corpus | warning recommended | A stylesheet function is called only from functions that are themselves never reached, so it can never run. Remove it, or call it from a template. |
| unused-function | corpus | warning recommended | A stylesheet function is never called in any expression across the corpus. Remove it or call it. |
| unused-named-template | corpus | warning | A named template is never invoked via xsl:call-template from anything that runs. Remove it or call it. |
| unused-variable | corpus | warning | A variable is declared but never referenced by $name across the corpus. Remove it or use it. |
| invalid-xpath-expression | validation | error recommended | An XPath expression is malformed and cannot be parsed. Fix its syntax. |
| malformed-stylesheet | validation | error recommended | The stylesheet is not well-formed XML and cannot be parsed. Fix its syntax. |
| syntax-newer-than-xslt-version | validation | error recommended | This XPath syntax belongs to a later XSLT version than the one in force, so a conformant processor rejects it. Raise the version, or respell the construct. |
| broken-href | format | error recommended | The @href names no file on disk, so no processor will load this stylesheet. Point it at the module, or restore the file. |
| circular-import | format | error recommended | The stylesheet pulls in itself, or a module that pulls it back, through xsl:import or xsl:include, which is a static error. Break the cycle. |
| confusing-variable-and-node | format | warning suggested fix | A variable name is used bare as a node selector. Use $name syntax to reference the variable. |
| count-compared-to-zero | format | warning safe fix | count(...) is compared with 0 to test existence, which walks the whole sequence. Test existence directly instead (exists()/empty() on XSLT 2.0+, boolean()/not() on 1.0). |
| leaking-result-namespace | format | warning suggested fix | The xsl:stylesheet declares a namespace prefix used only in stylesheet logic, which a literal result element copies into the output. Add the prefix to exclude-result-prefixes. |
| name-compared-to-string | format | warning suggested fix | name() or local-name() is compared with a string, which is prefix-fragile and slower than a node test. Match the element with a node test on the self axis instead. |
| not-creating-element-correctly | format | warning | xsl:element is used with a static name. Use a literal result element instead. |
| not-using-output | format | warning | The xsl:output instruction is missing. Declare it to specify the serialization format explicitly. |
| output-method-xml | format | warning suggested fix | xsl:output declares method='xml' while the root template builds an HTML document. Change the method to 'html'. |
| predicate-position-literal | format | warning safe fix | A positional predicate is written the long way; [position() = N] is just [N] and [position() = last()] is just [last()]. Shorten it. |
| redundant-boolean-call | format | warning safe fix | A boolean() call stands where nothing but a truth is taken, so the wrapper is redundant. Drop it. |
| redundant-double-negation | format | warning safe fix | A double negation not(not(x)) is redundant; it is just boolean(x), and where nothing but a truth is taken simply x. Replace it. |
| redundant-import | format | warning suggested fix recommended | The stylesheet imports or includes the same module more than once. Keep a single xsl:import/xsl:include of it and remove the other. |
| redundant-namespace-declarations | format | warning suggested fix | The xsl:stylesheet declares a namespace prefix that nothing in it uses. Remove the declaration, or exclude the prefix where a literal result element copies it out. |
| redundant-whitespace | format | warning safe fix | An XPath expression has redundant whitespace. Remove the extra spaces. |
| scans-whole-document | format | warning | An expression opens a path with //, which scans the whole document each time it runs. Name the path, use xsl:key, or bind the scan once in a top-level variable. |
| starts-with-double-slash | format | warning safe fix suggested fix | A pattern alternative opens with //, which at most demands a document-node root. Drop it, and set an explicit priority if the template must rank as before. |
| string-length-compared-to-zero | format | warning suggested fix | string-length(...) is compared with 0 to test emptiness, which measures the whole string. Compare the value with '' instead. |
| template-writes-nothing | format | warning recommended | This template declares variables and writes nothing to the result tree, so applying it produces nothing. Write the output it was meant to build, or empty it. |
| translate-for-case | format | warning suggested fix | translate() folds case over the ASCII alphabet alone. Use lower-case() or upper-case(), which are shorter and Unicode-aware. |
| unabbreviated-axis | format | warning safe fix | Verbose axis specifiers child::, attribute::, parent::node(), or self::node() are used. Replace them with the node name alone, @, .., or . respectively. |
| undefined-variable | format | error recommended | A variable is referenced where no binding in scope declares it. Declare it, or correct the name to one in scope. |
| unused-function-template-parameter | format | warning | A parameter is declared but never referenced in the function or template body. Remove it or use it. |
| use-double-slash | format | warning | A pattern contains //, which matches at any depth and is broader than intended. Use a specific path. |
| use-node-set-extension | format | warning safe fix | The node-set() extension function is unnecessary in XSLT 2.0 and later. Query the variable directly without it. |
| using-namespace-axis | format | warning | The namespace:: axis is deprecated in XSLT 2.0 and later. Use in-scope-prefixes() and namespace-uri-for-prefix() instead. |