XSLT 2.0で便利になった機能(45) xsl:with-param

これは便利になった機能というよりはむしろ失敗談なのですが、せっかく良い教訓を得たので紹介します.
 
それが起こったのは、次の日がスタイルシートの何回目かの中間リリースという夜でした.それまでばっちり動いていたスタイルシートに、何箇所かの「ちょっとした修正」を加えたのですが、その途端にSaxonがエラーを出して動かなくなってしまいました.表示されるエラーは次のようなものです.
 
  XTDE0420: Cannot create an attribute node whose parent is a document node
 
エラーの意味は「ドキュメントノードの下に属性ノードは作れない」ということになります.
 
SaxonはXSLTプロセッサの中では抜群にデバッグのための情報をちゃんと出してくれるプロセッサなのですが、このエラーはどうしてもわかりませんでした.
 
  at xsl:for-each (file:/D:/MyDocu%7E1/XML2012/Xxxx/DEV_XX/stylesheet/templates//output_lecture_skip.xsl#50)
 
という箇所を指しているのですが、fo-eachでそもそもこのエラーの意味しているドキュメントノードの下に属性ノードを作成している箇所なんてどう見てもないのです.
 
Saxonの起動パラメータに-Tをつけてステップトレースを出してみたのですが、これもやはり該当のfor-eachで止まっていてやはり原因がつかめません.時間はどんどん過ぎて行き、もう22時を回っています.もしかしたらSaxonのバグなのかと思って、9.2(お客さんのバージョン)⇒最新の9.4に変えてみても同じところで落ちてしまいます.
 
仕方がないのでSaxon-Helpのメーリングリストに投稿してみましたが、なかなか配信されません.(SourceForgeのMLは少し時間がかかるようです.)
 
あせる頭をもう一度整理して、手を加えた箇所をもう一度見直してみました.こういうときにWinMergeが威力を発揮してくれます.すると次のような修正をしている箇所が目に付きました.
 
<xsl:apply-templates select="t_u">
    <xsl:with-param name="prmTestAttr"">
        <xsl:call-template name="getAttributeSet">
            <xsl:with-param name="prmAttrSetName" select="'main_text_ans'"/>
        </xsl:call-template>
    </xsl:with-param>
</xsl:apply-templates>
 ↓
<xsl:apply-templates select="t_u">
    <xsl:with-param name="prmTestAttr">
        <xsl:copy-of select="$testAttr"/>
        <xsl:call-template name="getAttributeSet">
            <xsl:with-param name="prmAttrSetName" select="'main_text_ans'"/>
        </xsl:call-template>
    </xsl:with-param>
</xsl:apply-templates>
 
ここで、$testAttrはas="attribute()*"として型付けされている変数です.またgetAttributeSetテンプレートもas="attribute()*"として型付けされているテンプレートです.なんの気なしに付け加えた<xsl:copy-of>がエラーの原因でした.次のようにas属性を追加すると、エラーは出なくなりスタイルシートは期待通りに動いてくれるようになりました.
 
<xsl:apply-templates select="t_u">
    <xsl:with-param name="prmTestAttr" as="attribute()*">
        <xsl:copy-of select="$testAttr"/>
        <xsl:call-template name="getAttributeSet">
            <xsl:with-param name="prmAttrSetName" select="'main_text_ans'"/>
        </xsl:call-template>
    </xsl:with-param>
</xsl:apply-templates>
 
つまり原因はxsl:with-paramがちゃんと型付けされていなかったことにありました.
 
as属性がなくて型付けされておらず、select属性もないので、ドキュメントノードが作られて、そこに<xsl:copy-of-select="$testAttr">が動いてドキュメントノードの下に属性ノードを作ろうとして失敗して先ほどのエラーとなったのでしょう.
 
本来この$prmTestAttrは受け取り側のテンプレートではattribute()*となっていたのですが、xsl:apply-templateでは、テンプレートにより受け取るパラメータの種類を変えられるので、パラメータの型の検証ができないと考えられます.
 
XSLT2.0の仕様をもう一度見直してみたら、このことはちゃんと書かれていました.
 
9.3 Values of Variables and Parameters
http://www.w3.org/TR/xslt20/#variable-values
 
xsl:with-paramでas属性もselect属性もなければ、"Value is a document node whose content is obtained by evaluating the sequence constructor"となってしまうのです.
 
それでは、<xsl:copy-of-select="$testAttr">を入れる前はどうして動いていたのでしょう?これは、getAttributeSetテンプレートの戻り値がas="attribute()*"と明確に宣言されていたのでSaxonが、$prmTestAttrの型をattribute()*と解釈してくれたものでしょう.これはSaxonがXSLT2.0の仕様を超えて面倒を見てくれていたことになります.
 
仕様どおりにゆけば
 
<xsl:with-param name="prmVal">2</xsl:with-param>
 
とすれば、
 
<xsl:with-param name="prmVal" as="document-node()">
  <xsl:document>
    <xsl:value-of select="'2'"/>
  </xsl:document>
</xsl:with-param>
 
と書いているのと同じです.おそろしいですね.この上のコード、XSLT1.0の感覚のまま何もせずにXSLT2.0に移行したスタイルシートのコーディングに見られます.
 
XSLT2.0は仕組みを理解していないと思わぬところに落とし穴が待っていました.注意したいものです.