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を持っています。他のセレクタ・タイプの場合、ClassSelector、IdSelectorおよびPsuedoSelectorが使用されます。SelectorListには2つのSelectorノードが含まれています。これは、bodyおよびhtmlがcommaを使用して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プロパティ(文字列)は、ノードが表すものを指定します。たとえば、AssignmentExpressionのnode.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の任意の部分に構文ツリーを生成できるようにすることで非常に役立ちます。