8 カスタム・ノード・ルールの実装

ノード・ルールは、HTML、JSON、CSS、JavaScriptなどのアプリケーション・ファイルの解析に対応して作成する標準監査ルールです。Oracle JAFでは、Oracle JAF監査エンジンが抽象構文ツリー(AST)の形式で参照するデータ・ノードを作成することでファイル解析を処理し、カスタム・ノード・ルールに登録できるノード・イベント・リスナーを介して公開します。

CSS監査におけるASTルール・ノードについて

CSSファイルやHTMLファイルの<style>セクションを監査するルールは、実行時にnode.jsモジュールとしてロードされるJavaScript/TypeScriptファイルとして実装され、監査対象コンテンツの抽象構文ツリー(AST)が分析されて、監査ルールに登録したノード・タイプ・リスナーが呼び出されると、Oracle JAFからコンテキストが渡されます。

CSSでのルール・ノードの概要

次のCSSルールを考えてみます:

body,html {
  margin:0; padding:0;
}

CSSルールは、ASTではRuleノードとして表されます。次に、Ruleノードのスケルトン・ビューを示します:

{
   "type": "Rule",
    . . .
   "prelude" : {},             // see below
   "block" : {                 // contains the property/value pairs
      "children" : [
              {
                "type" : "Declaration",
                "property" : "margin"
                "value" : {
                     "children" : [
                            {
                              "type" : "number",
                              "value" : "0"
                            }
                      ]
                }
             },
             {
               "type" : "Declaration",
               "property" : "padding"
               "value" : {
                    "children" : [
                            {
                              "type" : "number",
                              "value" : "0"
                            }
                    ]
               }
            }
      ]
   }
} 

このサンプルによると、このRuleノードからプロパティ/値のペアを抽出することは単純なタスクです。

わかりやすくするために、前述のコンテンツの一部が省略されています。たとえば、Ruleノード全体で、位置情報を含むlocサブプロパティがあります:

"loc": {
    "source": " ",
       "start": {
           "offset": 18,
           "line": 3,
           "column": 5
       },
       "end": {
           "offset": 26,
           "line": 3,
           "column": 13
       }
}

ノート:

locの位置情報は、CSSテキストの開始位置に対して相対的です。CSSはHTMLの<style>にも埋め込むことができるため、ルール・コンテキストには、テキストの実際の起点を指定し、問題の報告時に位置情報を調整するために使用できるoffsetプロパティを指定します。「CSSルール・リスナーで使用可能なコンテキスト・オブジェクト・プロパティ」offsetプロパティおよびヘルパー・ユーティリティ・メソッドCssUtils.getPosition()の説明を参照してください。

このCSSルールの例では、プロパティpreludeが表示されています。これには、ルールの構造の上位のビューが含まれ、ノード・タイプSelectorListおよびSelectorが導入されます。スケルトンの例を次に示します。

"prelude": {
    "type": "SelectorList",
    "children": [
        {
          "type": "Selector",
          "children": [
               {
                 "type": "TypeSelector",
                 "name": "body"
               }
            ]
         },
         {
            "type": "Selector",
            "children": [
               {
                  "type": "TypeSelector",
                  "name": "html"
               }
            ]
         }
    ]
}

前述のサンプルでは、typeプロパティは要素<body>および<html>を参照しているため、値TypeSelectorを持っています。他のセレクタ・タイプの場合、ClassSelectorIdSelectorおよびPsuedoSelectorが使用されます。SelectorListには2つのSelectorノードが含まれています。これは、bodyおよびhtmlcommaを使用してCSS内でグループ化されているためです。SelectorListノードの詳細は、次を参照してください。

SelectorListノードの概要

前述のサンプルでは、preludeノードのSelectorListプロパティは、グループ化を使用した単純なケースで導入されました。この例では、SelectorListには、タイプTypeSelectorの2つのSelectorノードが含まれています。グループ化のカンマを使用したため、2つのSelectorノードが生成されました。この項では、combinatorsセレクタおよびpseudoセレクタを使用する場合について、さらに詳しく説明します。セレクタが結合されると、1つの複合Selectorのみが生成され、複数の子ノードが含まれます。

Combinatorの例

次の例を考えてみます:

.foo.bar  { ... }

これにより、SelectorListおよびSelectorノードのスケルトンが次のように生成されます。Selectorノードには、2つの子ClassSelectorノードが含まれています:

"prelude": {
  "type": "SelectorList",
  "children": [
     {
        "type": "Selector",
        "children": [
           {
              "type": "ClassSelector",
              "name": "foo"
           },
           {
              "type": "ClassSelector",
              "name": "bar"
           }
        ]
     }
  ]
}

次の例を考えてみます:

.foo .bar {...}

これにより、SelectorListおよびSelectorノードのスケルトンが次のように生成されます:

"prelude" : {
  "type": "SelectorList",
  "children": [
     {
        "type": "Selector",
        "children": [
           {
             "type": "ClassSelector",
             "name": "foo"
           },
           {
             "type": "WhiteSpace",
             "value": " "
           },
           {
             "type": "ClassSelector",
             "name": "h2"
           }
        ]
     }
  ]
}

次の例を考えてみます:

div > p  { ... }

これにより、次のSelectorListノードが生成されます:

"prelude" : {
  "type": "SelectorList",
  "children": [
     {
        "type": "Selector",
        "children": [
           {
             "type": "TypeSelector",
             "name": "div"
           },
           {
             "type": "Combinator",
             "name": ">"
           },
           {
             "type": "TypeSelector",
             "name": "p"
           }
        ]
     }
  ]
}

CSSごとに、2つのタイプ・セレクタの間にCombinatorノードが表示されます。

属性セレクタを使用した、この少し複雑な例を考えてみます:

a[href^="https"]  { ... }

これにより、SelectorListノードが次のように生成されます:

"prelude": {
   "type": "SelectorList",
   "children": [
      {
        "type": "Selector",
        "children": [
           {
             "type": "TypeSelector",
             "name": "a"
           },
           {
             "type": "AttributeSelector",
             "name": {
                "type": "Identifier",
                "name": "href"
             },
             "matcher": "^=",
             "value": {
                "type": "String",
                "value": "\"https\""
             }
           }
        ]
      }
   ]
}

前述のサンプルでは、matcherプロパティを持つAttributeSelectorノードが生成されています。

擬似クラス・セレクタの例

次の例を考えてみます:

.foo:focus  { . . . }

これにより、SelectorListおよびSelectorノードのスケルトンが次のように生成されます:

"prelude": {
  "type": "SelectorList",
  "children": [
     {
        "type": "Selector",
        "children": [
           {
             "type": "ClassSelector",
             "name": "foo"
           },
           {
             "type": "PseudoClassSelector",
             "name": "focus"
           }
        ]
     }
  ]
}

Selectorノードには、クラス・セレクタが反映され、その後に擬似クラス・セレクタが続きます。

次の例を考えてみます:

p:nth-last-child(2) {}

これにより、より複雑なSelectorノードが次のように生成されます:

"prelude": {
   "type": "SelectorList",
   "children": [
      {
        "type": "Selector",
        "children": [
           {
             "type": "TypeSelector",
             "name": "p"
           },
           {
             "type": "PseudoClassSelector",
             "name": "nth-last-child",
             "children": [
                {
                   "type": "Nth",
                   "nth": {
                      "type": "AnPlusB",
                      "a": null,
                      "b": "2"
                   }
                }
             ]
          }
        ]
      }
   ]
}

PseudoClassSelectorには、ノードが展開されています。

サンプルのHTMLおよびJSON監査ルールのウォークスルー

HTMLまたはJSONファイルを監査するルールは、監査対象ファイルの抽象構文ツリー(AST)が分析されて、HTML/JSON監査ルールに登録したノード・タイプ・リスナーが呼び出されると、Oracle JAFからコンテキストが渡されます。

このウォークスルーでは、最初の監査ルールは、HTMLを監査するルールの作成をいかに簡単に始めることができるかを示しています。後続のルール・サンプルでは、カスタム・ルールを作成する際の複雑さとOracle JAFの機能を示しています。全体として、Oracle JAFを使用すると、現在の位置からファイル内を前方または後方に検索できます。また、利用可能な様々なJAFユーティリティ関数により、ルールを作成するタスクが簡略化されます。

ノート:

わかりやすくするために、この項のサンプルでは、getName()getDescription()およびgetShortDescription()メソッドが省略されています。ノード・ルールの実装の基本を理解するには、「カスタム監査ルールの構造の理解」を参照してください。

バージョン1 - id属性の検証

この単純な導入ルールでは、すべての要素のid属性を調べて、プロジェクトの共通の接頭辞(acv-)で始まることを確認する必要があります。

... // for clarity, the getName(), getDescription(), and getShortDescription() methods have been omitted
 
function register(regContext)
{
  return { attr : _fnAttrs };
};
 
function _fnAttrs(ruleContext, attrName, attrValue)
{
  let issue;
 
  if ((attrName === "id") && (! attrValue.startsWith("acv-")))
  {
    issue = new ruleContext.Issue(`'id' attribute ('${attrValue}') is not prefixed with project prefix \"acv\"`);
    ruleContext.reporter.addIssue(issue, ruleContext);
  }
};

バージョン2 - id属性の検証

一般に、コンテキストで追加情報を調べることができるため、このルールでは、ACVで始まるファイル・セット内の特定のプロジェクト・ファイルのみを検索すると仮定します。ruleContextオブジェクトには、使用可能なメンバーfilepathがあります。filepathは、プラットフォームに関係なく常にフォワード・スラッシュを使用するため、/ACVのテストはすべてのプラットフォームで成功します。

function _fnAttrs(ruleContext, attrName, attrValue)
{
   let issue;
 
   if (ruleContext.filepath.includes("/ACV") && (attrName === "id") && (! attrValue.startsWith("acv-")))
   {
     issue = new ruleContext.Issue(`'id' attribute ('${attrValue}') is not prefixed with project prefix \"acv\"`);
     ruleContext.reporter.addIssue(issue, ruleContext);
   }
};

バージョン3 - id属性の検証

ルールを改善する方法です。JAFはファイル処理時に非常に効率的であるため、多数のファイルが関与している場合は、パフォーマンスの向上を図ることができます。これを行うには、コンテキスト・オブジェクトのnodeプロパティと、ノードのattribsプロパティを使用します。nodeプロパティはファイル内の現在のノードであるため、そこから前後に移動できます。また、パフォーマンスの観点から、属性ではなくHTML要素のみをリスニングすることで、ルールの呼出し回数を減らすことができます。平均して、DOM要素に5つの属性があると仮定すると、ルール呼出しの数は80%減少することになります。このバージョンのルールでは、各要素の属性が直接調べられます。

function register(regContext)
{
  // Listen for DOM elements instead of attribute
  return { tag : _fnTags };
};
 
function _fnTags(ruleContext, tagName)
{
  // Look at the element's attributes
  let attribs, attrValue, issue;
 
  // 'attribs' is an object of attribute name/value properties for the tag
  attribs   = ruleContext.node.attribs;
  // Get the 'id' value if it exists
  attrValue = attribs.id;              
 
  if (attrValue && (! attrValue.startsWith("acv-")))
  {
    issue = new ruleContext.Issue(`'id' attribute ('${attrValue}') is not prefixed with project prefix \"acv\"`);
    ruleContext.reporter.addIssue(issue, ruleContext);
  }
};

バージョン4 - id属性の検証

この時点で、ruleContextオブジェクトは、有用なDOMユーティリティ関数のコレクションであるDomUtilsへのアクセスを提供することに注意してください。たとえば、前述の例の関数_fnTags()は、次のように書き換えることができます。

function _fnTags(ruleContext, tagName)
{
  let attrValue, issue;
 
  // Returns the 'id' attribute's value if found
  attrValue = ruleContext.DomUtils.getAttribValue(context.node, "id");      
  if (attrValue && (! attrValue.startsWith("acv-")))
  {
    issue = new ruleContext.Issue(`'id' attribute ('${attrValue}') is not prefixed with project prefix \"acv\"`);
    ruleContext.reporter.addIssue(issue, ruleContext);
  }
};

バージョン5 - id属性の検証

JAFは効率的ですが、監査ルールはいつでも改善できます。ファイルの呼出しをリスニングするには、ルールはfileタイプのリスナーを登録する必要があります。

ノート:

パフォーマンスとルールの複雑性/保守性はトレードオフであることを理解する必要があります。たとえば、「カスタム・フック・ルールの実装」で説明されているように、ルールをフック・タイプ・ルールに変換することで、このルールの呼出し回数をファイル当たり1回に減らすことができます。基本的に、フック・ルールを使用すると、ファイルが最初に読み取られたときに、他のルールよりも前に、ルールを呼び出すようにリクエストできます(1回のみ)。つまり、ルールは解析されたノードを調べて、要素とその属性を見つける必要があります。

function register(regContext)
{
   // Listen for files instead of elements or attributes
   return { file : _fnFiles };
};
 
function _fnFiles(ruleContext)
{
  let tagNodes, node, attrValue, i;
  const DomUtils = ruleContext.utils.DomUtils;
 
  // Get elem nodes only (ignore text, comments, directives, etc)
  tagNodes = DomUtils.getElems() ;
  for (i = 0; i < tagNodes.length; i++)
  {
    node = tagNodes[i] ;
    // Get the id" attribute value
    attrValue = DomUtils.getAttribValue(node, "id");    
    if (attrValue && (! attrValue.startsWith("acv-")))
    {
       issue = new ruleContext.Issue(`'id' attribute ('${attrValue}') is not prefixed with project prefix \"acv\"`);
       ruleContext.reporter.addIssue(issue, ruleContext);
    }
  }
};

サンプルのCSS監査ルールのウォークスルー

CSSファイルやHTMLファイルの<style>セクションを監査するルールは、監査対象ファイルの抽象構文ツリー(AST)が分析されて、CSS監査ルールに登録したノード・タイプ・リスナーが呼び出されると、Oracle JAFからコンテキストが渡されます。

このCSSルールのウォークスルーのCSSは次のとおりです。

p ...  {
          color : "#112233",
          ...
        }

pは追加のCSS構文で装飾することができ、監査ルールではそのような装飾を無視する必要があります。

監査ルールは、CSSルールのリスニングから開始し、pタイプ・セレクタを探します。タイプ・セレクタの決定の詳細は、「CSS監査におけるASTルール・ノードについて」を参照してください。

次に、CSSの監査ルールの基本フレームワークを示します:

var  CssUtils ;
 
function  register(regCtx)
{
   // See setPosition() below 
   CssUtils.regCtx.utils.CssUtils;
   return { "css-rule" : _onRule }
};
 
 
function  _onRule(ruleCtx, rule)
{ 
   // If the rule has a p type selector
   if (_hasParaTypeSelector(rule))
   {
      // and the rule sets the 'color' property
      let loc = _getColorPropPos(rule);
      if (loc)
      {
         // report the issue.
         _emitIssue(ruleCtx, loc); 
      }
   }
};
 
 
function  _emitIssue(ruleCtx, loc)
{
   var issue = new ruleCtx.Issue("p type selector must not override the 'color' property");
 
   issue.setPosition(CssUtils.getPosition(ruleCtx, loc));
   ruleCtx.reporter.addIssue(issue, ruleCtx);
};

次のステップでは、ルール・ノードを分析して、pタイプ・セレクタを見つけます:

function  _hasParaTypeSelector(rule)
{
  var sels, sel, a, ch, i, j;
 
  if (rule.prelude.type === "SelectorList")
  {
    a = rule.prelude.children;
 
    for (i = 0; i < a.length; i++)
    {
       sels = a[i];
       if (sels.type === "Selector")
       {
         ch = sels.children;
         for (j = 0; j < ch.length; j++)
         {
           sel = ch[j];
           if (sel.type === "TypeSelector" && sel.name === "p")
           {
             return true;
           }
         }
       }
    }
  }
};

最後に、ルールを検索して、colorプロパティが指定されているかどうかを確認する必要があります:

function  _getColorPropPos(rule)
{
   var block, decl, i;
 
   // Process the rule's block of property/value pairs
   block = rule.block.children;
   for (i = 0; i < block.length; i++)
   {
      decl = block[i];
      if (decl.type === "Declaration" && decl.property === "color")
      {
         // Return the 'color' property position for the Issue
         return decl.loc;
      }
   }
};

サンプルのMarkdown監査ルールのウォークスルー

JAFは、Markdownファイルを解析して抽象構文ツリー(AST)にします。このASTはその後分析され、要約データ・オブジェクトが登録済のルール・リスナーを介してルールに提示されます。

Markdown処理では、ルールはfileイベント(つまり、.mdファイルが最初に読み取られるとき)、または特定のMarkdownイベント(特定のタイプのマークアップが見つかったとき)をリスニングできます。

Markdownルール・リスナーのリストとその引数の説明については、「Markdownルールのリスナー・タイプ」を参照してください。

どちらの場合も、ルール・リスナーが呼び出されると、contextオブジェクトが渡されます。すべてのルール・タイプのcontextオブジェクトで使用可能な多くのプロパティ(「登録済リスナーで使用可能なコンテキスト・オブジェクト・プロパティ」を参照)に加えて、contextには補助データ・プロパティsuppDataが含まれます。これは特に、Markdownファイルを監査する際に重要です。このプロパティを使用すると、そのutilsオブジェクトで使用可能なメソッドを介して、要約データ(リンク、イメージ、段落、ヘッダー、コード・ブロックなど)に簡単にアクセスできます。詳細は、「Markdownルール・リスナーで使用可能なコンテキスト・オブジェクト・プロパティ」を参照してください。

ノート:

fileイベントをsuppData.utilsを介して使用可能なユーティリティ・メソッドと組み合せて使用すると、その時点ですべての要約データが利用でき、ASTを検査する必要がなくなるため、ASTをたどることなく最も簡単に要約データにアクセスできます。

Markdownイベントのリスニング

Markdownイベントの形式はmd-xxxxxです。ここで、xxxxxは必要なMarkdownデータのタイプを表します(たとえば、リンク・イベントの場合はmd-linkで、次のルール・クラスで使用されます)。

register(regCtx)
{
   return {
             md-link : this._onLink,      // Want markup URL references
             . . .
          }
}
 
_onLink(ruleCtx, link)
{
   // Process the link object passed as the second argument
   . . .
 
   // Access to all the parsed data is also available via the supplementary data object
   var utils  = ruleCtx.suppData.utils ;
   var images = utils.getImages() ;         // Returns an array of image objects
 
   // Process the links array
   . . .
}

補助データ・プロパティsuppDataを使用してMarkdown utilsオブジェクトを取得する方法に注意してください。これは、マークアップ・テキストからイメージ・データを取得するために使用されます。

ファイル・フックを使用することもできます。これにより、ルールはすべてのMarkdownデータに同時にアクセスできるようになります。

register(regCtx)
{
   return {
             file : this._onFile,      // Listen for files being read
             . . .
          }
}
 
_onFile(ruleCtx)
{
   var utils, links, images, paras ;
 
   utils  = ruleCtx.suppData.utils ;
   links  = utils.getLinks() ;         // Array of link objects
   images = utils.getImages() ;        // Array of image objects
   paras  = utils.getParas() ;         // Array of paragraph objects 
 
   // Process the links, images, and paragraphs
   . . .
}

ルールの例

次のルールは、Markdownの最初の段落に著作権が含まれていることを確認します。

class  Rule
{
  // For clarity, getRule(), getDescription(), and getShortDescription() have been omitted.
 
  register(regCtx)
  {
    return { file : _onFile }
  }
 
  _onFile(ruleCtx)
  {
    var  utils  = ruleCtx.suppData.utils ;
    var  paras  = utils.getParas() ;
 
    var para = paras[0] ;                   // Get the first paragraph.
 
    if (! /[Oo]racle/.test(para.text))
    {
      let issue = new ruleCtx.Issue("Copyright must be declared in first paragraph") ;
 
      // Supply start and end indices so that the paragraph can be highlighted.
      issue.setPosition(null, null, paras.pos[0].start, paras.pos[1].end);     // JAF will compute the line/col from the start index
      ruleCXtx.reporter.addIssue(issue, ruleCtx) ;
    }
  }
 
}

次のルールは、"Oracle"を含むURLへのすべての参照を検索します。

class Rule
          {
          // For clarity, getRule(),getDescription(), and getShortDescription() have been omitted  
          
          register(regCtx)
           {
            return { file : _onFile}
            }

          _onFile(ruleCtx) 
          {
             var  utils    = ruleCtx.suppData.utils;
             var  links    = utils.getLinks();
             var  refLinks = utils.getRefLinks();
             var  images   = utils.getImages();
          
             // URLs are found in inline-links, inline-images, and reference links
    
             // Inspect inline links
              links.forEach( (link) => {         
                               if (checkUrl(link.inline)       
                                 {   
                                   // found                            
                                  }                           
                                 });
             // Inspect reference links
                for (const ref in refLinks)
                  {
                    link = refLinks[ref] 
                    if (checkUrl(link, refLink)     
                    {
                      // found                              
                     } 
          
              // Inspect inline-image links
                 images.forEach( (link) => {         
                                    if (checkUrl(link.inline)                                    if(link.link.includes("Oracle"))         
                                    {     
                                           // found
                                     }                           
                                });
         }  
 
      _checkUrl(link, refLink)
        {
             if (link.inline || refLink)         // Inline links contain a url (i.e., ignore references to links)
             {     
               return (link.link.includes("Oracle")) ;
              }  
         } 
    };

サンプルのJavaScript/TypeScript監査ルールのウォークスルー

JavaScript/TypeScriptファイルを監査するルールは、監査対象ファイルの抽象構文ツリー(AST)が分析されて、JavaScript/TypeScript監査ルールに登録したノード・タイプ・リスナーが呼び出されると、Oracle JAFからコンテキストが渡されます。

JavaScript/TypeScriptファイルは、JAFによって抽象構文ツリー(AST)に解析されます。以降、ツリーが参照されると、ルールによって登録されたタイプのノードがcontext.nodeのルールに渡されます。node.typeプロパティ(文字列)は、ノードが表すものを指定します。たとえば、AssignmentExpressionnode.typeは、myVariable = 42などの典型的な文の形式を示しています。例として、JavaScriptでは、この文を表すASTの部分は次のとおりです:

myVariable = 42;

前述の文は、次のノードに解析されます。ここでは、わかりやすくするために、いくつかの追加プロパティが削除されています:

{
   "type":  "ExpressionStatement",
   "expression":  {
                     "type":  "AssignmentExpression",
                     "operator": "=",
                     "left":   {
                                 "type": "Identifier",
                                 "name":  "myVariable",
                               },
                     "right":  {
                                  "type": "Literal",
                                  "value": 42,
                                  "rawValue": 42,
                              },
                  },
}

したがって、42を超える変数に対する番号の割当てにフラグを付ける単純なルールは、次のようになります:

function register(regContext)
{
   return {
             AssignmentExpression : _fnAssign
          }; 
};
  
function _fnAssign(ruleCtx, node)
{
  if (node.left && (node.left.type === "Identifier"))
  {
    if (node.right && (node.right.type === "Literal") && (parseInt(node.right.value) > 42))
    {
       let issue = new ruleCtx.Issue(`${node.left.name} assignment is greater than 42`);
       ruleCtx.reporter.addIssue(issue, ruleCtx);
    }
  }
};

ヒント:

JavaScriptルールを作成する場合、監査対象の特定のケースの構文ツリーを参照できると便利です。AST Explorerツールは、JavaScriptの任意の部分に構文ツリーを生成できるようにすることで非常に役立ちます。