@angular-eslint/eslint-plugin-template 4.2.2-alpha.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [4.3.0](https://github.com/angular-eslint/angular-eslint/compare/v4.2.1...v4.3.0) (2021-05-12)
7
+
8
+ ### Features
9
+
10
+ - **eslint-plugin-template:** add rule accessibility-label-has-associated-control ([#392](https://github.com/angular-eslint/angular-eslint/issues/392)) ([0851f3e](https://github.com/angular-eslint/angular-eslint/commit/0851f3eeda54c8c9ad01460b91cf8cf67017f1db))
11
+
6
12
  ## [4.2.1](https://github.com/angular-eslint/angular-eslint/compare/v4.2.0...v4.2.1) (2021-05-12)
7
13
 
8
14
  ### Bug Fixes
@@ -4,6 +4,7 @@
4
4
  "@angular-eslint/template/accessibility-alt-text": "error",
5
5
  "@angular-eslint/template/accessibility-elements-content": "error",
6
6
  "@angular-eslint/template/accessibility-label-for": "error",
7
+ "@angular-eslint/template/accessibility-label-has-associated-control": "error",
7
8
  "@angular-eslint/template/accessibility-table-scope": "error",
8
9
  "@angular-eslint/template/accessibility-valid-aria": "error",
9
10
  "@angular-eslint/template/banana-in-box": "error",
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { RULE_NAME as accessibilityAltTextRuleName } from './rules/accessibility-alt-text';
2
2
  import { RULE_NAME as accessibilityElementsContentRuleName } from './rules/accessibility-elements-content';
3
3
  import { RULE_NAME as accessibilityLabelForRuleName } from './rules/accessibility-label-for';
4
+ import { RULE_NAME as accessibilityLabelHasAssociatedControlRuleName } from './rules/accessibility-label-has-associated-control';
4
5
  import { RULE_NAME as accessibilityTableScopeRuleName } from './rules/accessibility-table-scope';
5
6
  import { RULE_NAME as accessibilityValidAriaRuleName } from './rules/accessibility-valid-aria';
6
7
  import { RULE_NAME as bananaInBoxRuleName } from './rules/banana-in-box';
@@ -25,6 +26,7 @@ declare const _default: {
25
26
  "@angular-eslint/template/accessibility-alt-text": string;
26
27
  "@angular-eslint/template/accessibility-elements-content": string;
27
28
  "@angular-eslint/template/accessibility-label-for": string;
29
+ "@angular-eslint/template/accessibility-label-has-associated-control": string;
28
30
  "@angular-eslint/template/accessibility-table-scope": string;
29
31
  "@angular-eslint/template/accessibility-valid-aria": string;
30
32
  "@angular-eslint/template/banana-in-box": string;
@@ -79,6 +81,13 @@ declare const _default: {
79
81
  readonly labelAttributes?: readonly string[] | undefined;
80
82
  readonly labelComponents?: readonly string[] | undefined;
81
83
  }], import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
84
+ "accessibility-label-has-associated-control": import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleModule<"accessibilityLabelHasAssociatedControl", [{
85
+ readonly controlComponents?: readonly string[] | undefined;
86
+ readonly labelComponents?: readonly {
87
+ readonly inputs?: readonly string[] | undefined;
88
+ readonly selector: string;
89
+ }[] | undefined;
90
+ }], import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
82
91
  "accessibility-table-scope": import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleModule<"accessibilityTableScope", [], import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
83
92
  "accessibility-valid-aria": import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleModule<import("./rules/accessibility-valid-aria").MessageIds, [], import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
84
93
  "banana-in-box": import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleModule<"bananaInBox", [], import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var e=require("typescript"),t=require("@typescript-eslint/experimental-utils"),n=require("@angular/compiler"),a=require("aria-query");function r(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=r(e);function s(e,t){const n=(e=e.replace(/\r\n/g,"\n")).indexOf(t);return[n,n+t.length]}const i=new Map;var c={"extract-inline-html":{preprocess:function(e,t){const n=[e];if(!function(e,t){return!![".component.ts",".page.ts",".dialog.ts",".modal.ts",".popover.ts",".bottomsheet.ts",".snackbar.ts"].some(e=>t.endsWith(e))||!(!e.includes("Component")||!e.includes("@angular/core"))}(e,t))return n;try{const a=o.default.createSourceFile(t,e,o.default.ScriptTarget.Latest,!0),r=a.statements.filter(e=>o.default.isClassDeclaration(e));if(!r||!r.length)return n;const c=[];for(const e of r)if(e.decorators)for(const t of e.decorators)o.default.isCallExpression(t.expression)&&o.default.isIdentifier(t.expression.expression)&&"Component"===t.expression.expression.text&&c.push(t);if(!c||!c.length)return n;const l=[e];let u=0;for(const t of c){if(!o.default.isDecorator(t)||!o.default.isCallExpression(t.expression)||1!==t.expression.arguments.length)continue;const n=t.expression.arguments[0];if(!o.default.isObjectLiteralExpression(n))continue;const r=n.properties.find(e=>e&&e.name&&"template"===e.name.getText());if(n.properties.find(e=>e&&e.name&&"templateUrl"===e.name.getText())||!r)continue;if(!o.default.isPropertyAssignment(r)||!o.default.isStringLiteralLike(r.initializer))continue;const c=r.initializer.text,m=s(e,c),p=`inline-template-${++u}.component.html`;i.set(p,{range:m,lineAndCharacter:{start:a.getLineAndCharacterOfPosition(m[0]),end:a.getLineAndCharacterOfPosition(m[1])}}),l.push({text:c,filename:p})}return l}catch(e){return console.log(e),console.error("preprocess: ERROR could not parse @Component() metadata",t),n}},postprocess:function(e,t){const n=e[0];if(1===e.length)return n;const a=e.slice(1);return[...n,...[].concat(...a.map((e,t)=>{const n=`inline-template-${++t}.component.html`,a=i.get(n);return a?e.map(e=>{if(e.line=e.line+a.lineAndCharacter.start.line,e.endLine=e.endLine+a.lineAndCharacter.start.line,e.fix){const t=a.range[0];e.fix.range=[t+e.fix.range[0],t+e.fix.range[1]]}return e}):[]}))]},supportsAutofix:!0}};const l=t.ESLintUtils.RuleCreator(e=>"https://github.com/angular-eslint/angular-eslint");function u(e){return m(e),e.parserServices}function m(e){var t,n;if(null==(t=e.parserServices)||!t.convertNodeSourceSpanToLoc||null==(n=e.parserServices)||!n.convertElementSourceSpanToLoc)throw new Error("You have used a rule which requires '@angular-eslint/template-parser' to be used as the 'parser' in your ESLint config.")}const p=Symbol("PROPERTY");function d(e,t){const a=e.attributes.find(e=>e.name===t);if(a)return a.value;const r=e.inputs.find(e=>e.name===t);return r&&r.value instanceof n.ASTWithSource?r.value.ast instanceof n.LiteralPrimitive?r.value.ast.value:p:null}var g=l({name:"accessibility-alt-text",meta:{type:"suggestion",docs:{description:"Enforces alternate text for elements which require the alt, aria-label, aria-labelledby attributes.",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityAltText:"<{{element}}/> element must have a text alternative."}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(img|area|object|input)$/]"(n){if(!function(e){return"img"===e.name?function(e){return e.attributes.some(({name:e})=>b(e))||e.inputs.some(({name:e})=>b(e))}(e):"object"===e.name?function(e){let t=!1,n=!1;for(const a of e.attributes)t="title"===a.name,n=y(a.name);if(t||n)return!0;let a=!1,r=!1;for(const t of e.inputs)a="title"===t.name,r=y(t.name);return!(!a&&!r)||e.children.length>0&&!!e.children[0].value}(e):"area"===e.name?f(e):function(e){return"image"!==d(e,"type")||f(e)}(e)}(n)){const a=t.convertElementSourceSpanToLoc(e,n);e.report({loc:a,messageId:"accessibilityAltText",data:{element:n.name}})}}}}});function f(e){let t=!1,n=!1;for(const a of e.attributes)t=b(a.name),n=y(a.name);if(t||n)return!0;let a=!1,r=!1;for(const t of e.inputs)a=b(t.name),r=y(t.name);return a||r}function y(e){return"aria-label"===e||"aria-labelledby"===e}function b(e){return"alt"===e}function h(e){if("string"==typeof e){if("true"===e)return!0;if("false"===e)return!1}return e}function x(e){if("INPUT"===e.name.toUpperCase()){const t=d(e,"type");if("string"==typeof t&&"HIDDEN"===t.toUpperCase())return!0}const t=h(d(e,"aria-hidden"));return""===t||t===p||!0===t}const v=new Set(["aria-label","innerHtml","innerHTML","innerText","outerHTML","title"]);var S=l({name:"accessibility-elements-content",meta:{type:"suggestion",docs:{description:"Ensures that the heading, anchor and button elements have content in it",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityElementsContent:"<{{element}}> should have content"}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(a|button|h1|h2|h3|h4|h5|h6)$/][children.length=0]"(n){if(x(n))return;const{attributes:a,inputs:r,name:o,sourceSpan:s}=n;if([...a,...r].map(({name:e})=>e).some(e=>v.has(e)))return;const i=t.convertNodeSourceSpanToLoc(s);e.report({loc:i,messageId:"accessibilityElementsContent",data:{element:o}})}}}});const A={items:{type:"string"},type:"array",uniqueItems:!0},T=["button","input","meter","output","progress","select","textarea"],E=["for","htmlFor"],I=["label"];var C=l({name:"accessibility-label-for",meta:{type:"suggestion",docs:{description:"Ensures that a label element/component is associated with a form element",category:"Best Practices",recommended:!1},schema:[{additionalProperties:!1,properties:{controlComponents:A,labelAttributes:A,labelComponents:A},type:"object"}],messages:{accessibilityLabelFor:"A label element/component must be associated with a form element"}},defaultOptions:[{controlComponents:T,labelAttributes:E,labelComponents:I}],create(e,[t]){const a=u(e),{controlComponents:r,labelAttributes:o,labelComponents:s}=function({controlComponents:e,labelAttributes:t,labelComponents:n}){return{controlComponents:new Set([...T,...null!=e?e:[]]),labelAttributes:new Set([...E,...null!=t?t:[]]),labelComponents:new Set([...I,...null!=n?n:[]])}}(t);var i;return{[`Element[name=${i=[...s],RegExp(`^(${i.join("|")})$`)}]`](t){const s=new Set([...t.attributes,...t.inputs].map(({name:e})=>e));if([...o].some(e=>s.has(e))||function(e,t){return Boolean([...e].some(e=>{return a=e,function e({children:t}){return t.some(t=>t instanceof n.TmplAstElement&&(t.name===a||e(t)))}(t);var a}))}(r,t))return;const i=a.convertNodeSourceSpanToLoc(t.sourceSpan);e.report({loc:i,messageId:"accessibilityLabelFor"})}}}}),k=l({name:"accessibility-table-scope",meta:{type:"suggestion",docs:{description:"Ensures that scope is not used on any element except <th>",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityTableScope:"Scope attribute can only be on <th> element"}},defaultOptions:[],create(e){const t=u(e);return{'Element[name!="th"] > :matches(BoundAttribute[name="scope"], TextAttribute[name="scope"])'({sourceSpan:n}){const a=t.convertNodeSourceSpanToLoc(n);e.report({loc:a,messageId:"accessibilityTableScope"})}}}}),B=l({name:"accessibility-valid-aria",meta:{type:"suggestion",docs:{description:"Ensures that correct ARIA attributes and respective values are used",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityValidAria:"The `{{attribute}}` is an invalid ARIA attribute",accessibilityValidAriaValue:"The `{{attribute}}` has an invalid value. Check the valid values at https://raw.githack.com/w3c/aria/stable/#roles"}},defaultOptions:[],create(e){const t=u(e);return{"BoundAttribute[name=/^aria-.*/], TextAttribute[name=/^aria-.*/]"(r){const{name:o,sourceSpan:s}=r,i=a.aria.get(o),c=t.convertNodeSourceSpanToLoc(s);if(!i)return void e.report({loc:c,messageId:"accessibilityValidAria",data:{attribute:o}});const l=function(e){return e instanceof n.TmplAstBoundAttribute?e.value.ast:e}(r);(function(e){return!function(e){return e instanceof n.LiteralArray||e instanceof n.LiteralMap}(e)&&!function(e){return e instanceof n.LiteralPrimitive||e instanceof n.TmplAstTextAttribute}(e)})(l)||function({allowundefined:e,type:t,values:n},a){if(e&&L(a))return!0;switch(t){case"boolean":return w(a);case"tristate":return w(a)||L(a);case"id":case"idlist":return!0;case"integer":return r=a,!Number.isNaN(r)&&parseInt(Number(r))==r&&!Number.isNaN(parseInt(r,10));case"number":return function(e){return!Number.isNaN(Number.parseFloat(e))&&Number.isFinite(e)}(a);case"string":return function(e){return"string"==typeof e}(a);case"token":case"tokenlist":{const e=w(a)?JSON.parse(a):a;return Boolean(null==n?void 0:n.includes(e))}}var r}(i,l.value)||e.report({loc:c,messageId:"accessibilityValidAriaValue",data:{attribute:o}})}}}});function w(e){return"boolean"==typeof e||"false"===e||"true"===e}function L(e){return null==e}const N=/\[(.*)\]/;var P=l({name:"banana-in-box",meta:{type:"suggestion",docs:{description:"Ensures that the two-way data binding syntax is correct",category:"Best Practices",recommended:"error"},fixable:"code",schema:[],messages:{bananaInBox:"Invalid binding syntax. Use [(expr)] instead"}},defaultOptions:[],create(e){const t=u(e),n=e.getSourceCode();return{BoundEvent({name:a,sourceSpan:r}){const o=a.match(N);if(!o)return;const[,s]=o,i=`[(${s})]`,c=t.convertNodeSourceSpanToLoc(r),l=n.getIndexFromLoc(c.start);e.report({messageId:"bananaInBox",loc:c,fix:e=>e.replaceTextRange([l,l+a.length+2],i)})}}}});let O=null;function F(){return O||(O=new Set(a.dom.keys()))}const $=new Set(["presentation","none",p]);let j=null,q=null,D=null;var R=l({name:"click-events-have-key-events",meta:{type:"suggestion",docs:{description:"Ensures that the click event is accompanied with at least one key event keyup, keydown or keypress.",category:"Best Practices",recommended:!1},schema:[],messages:{clickEventsHaveKeyEvents:"click must be accompanied by either keyup, keydown or keypress event for accessibility."}},defaultOptions:[],create:e=>({Element(t){if(!F().has(t.name))return;if(function(e){const t=d(e,"role");return null!==t&&$.has(t)}(t)||x(t)||function(e){return F().has(e.name)&&function(e){function t(t){return e.name===t.name&&function(e=[],t){const n=[...t.attributes,...t.inputs];return e.every(e=>n.some(n=>"a"===t.name&&"routerLink"===n.name||e.name===n.name&&(!e.value||e.value===h(d(t,e.name)))))}(t.attributes,e)}return!!function(){if(null===j){const e=[...a.roles.keys()],t=[...a.elementRoles],n=new Set(e.filter(e=>{const t=a.roles.get(e);return!t.abstract&&"progressbar"!==e&&t.superClass.some(e=>e.includes("widget"))}).concat("toolbar"));j=t.reduce((e,[t,a])=>([...a].some(e=>n.has(e))&&e.push(t),e),[])}return j}().some(t)||!function(){if(null===q){const e=[...a.roles.keys()],t=[...a.elementRoles],n=new Set(e.filter(e=>{const t=a.roles.get(e);return!t.abstract&&"toolbar"!==e&&!t.superClass.some(e=>e.includes("widget"))}).concat("progressbar"));q=t.reduce((e,[t,a])=>([...a].every(e=>n.has(e))&&e.push(t),e),[])}return q}().some(t)&&!!function(){if(null===D){const{AXObjects:e,elementAXObjects:t}=require("axobject-query"),n=new Set(Array.from(e.keys()).filter(t=>"widget"===e.get(t).type));D=[...t].reduce((e,[t,a])=>([...a].every(e=>n.has(e))&&e.push(t),e),[])}return D}().some(t)}(e)}(t))return;let n=!1,r=!1;for(const e of t.outputs)n="click"===e.name,r=e.name.startsWith("keyup")||e.name.startsWith("keydown")||e.name.startsWith("keypress");if(!n||r)return;const o=u(e).convertNodeSourceSpanToLoc(t.sourceSpan);e.report({loc:o,messageId:"clickEventsHaveKeyEvents"})}})}),M=l({name:"conditional-complexity",meta:{type:"suggestion",docs:{description:"The conditional complexity should not exceed a rational limit",category:"Best Practices",recommended:!1},schema:[{type:"object",properties:{maxComplexity:{minimum:1,type:"number"}},additionalProperties:!1}],messages:{"conditionalСomplexity":'The conditional complexity "{{totalComplexity}}" exceeds the defined limit "{{maxComplexity}}"'}},defaultOptions:[{maxComplexity:5}],create(e,[{maxComplexity:t}]){m(e);const a=e.getSourceCode();return{BoundAttribute(r){if(!r.value.source)return;const o=U(H((W||(W=new n.Parser(new n.Lexer))).parseBinding(r.value.source,"",0).ast));if(o<=t)return;const{sourceSpan:{start:s,end:i}}=r.value;e.report({loc:{start:a.getLocFromIndex(s),end:a.getLocFromIndex(i)},messageId:"conditionalСomplexity",data:{maxComplexity:t,totalComplexity:o}})},Interpolation({expressions:n}){for(const r of n){const n=U(r);if(n<=t)continue;const{sourceSpan:{start:o,end:s}}=r;e.report({loc:{start:a.getLocFromIndex(o),end:a.getLocFromIndex(s)},messageId:"conditionalСomplexity",data:{maxComplexity:t,totalComplexity:n}})}}}}});function H(e){return e instanceof n.BindingPipe?e.exp:e}let W=null;function U(e){const t=H(e);if(!(t instanceof n.Binary||t instanceof n.Conditional))return 0;let a=1;return t instanceof n.Binary&&(t.left instanceof n.Binary&&(a+=U(t.left)),t.right instanceof n.Binary&&(a+=U(t.right))),t instanceof n.Conditional&&(a+=U(t.condition)+U(t.trueExp)+U(t.falseExp)),a}var V=l({name:"cyclomatic-complexity",meta:{type:"suggestion",docs:{description:"Checks cyclomatic complexity against a specified limit. It is a quantitative measure of the number of linearly independent paths through a program's source code",category:"Best Practices",recommended:!1},schema:[{type:"object",properties:{maxComplexity:{type:"number",minimum:1}},additionalProperties:!1}],messages:{cyclomaticComplexity:'The cyclomatic complexity "{{totalComplexity}}" exceeds the defined limit "{{maxComplexity}}"'}},defaultOptions:[{maxComplexity:5}],create(e,[{maxComplexity:t}]){let n=0;const a=u(e);return{'BoundAttribute[name=/^(ngForOf|ngIf|ngSwitchCase)$/], TextAttribute[name="ngSwitchDefault"]'({sourceSpan:r}){if(n+=1,n<=t)return;const o=a.convertNodeSourceSpanToLoc(r);e.report({messageId:"cyclomaticComplexity",loc:o,data:{maxComplexity:t,totalComplexity:n}})}}}});const _=/[a-z]/i,z=new Set(["charset","class","color","colspan","fill","formControlName","height","href","id","lang","src","stroke","stroke-width","style","svgIcon","tabindex","target","type","viewBox","width","xmlns"]),K={checkAttributes:!0,checkId:!0,checkText:!0,ignoreAttributes:[...z]};var X=l({name:"i18n",meta:{type:"suggestion",docs:{description:"Helps to ensure following best practices for i18n. Checks for missing i18n attributes on elements and non-ignored attributes containing text. Can also highlight tags that do not use custom ID (@@) feature. ",category:"Best Practices",recommended:!1,suggestion:!0},fixable:"code",schema:[{type:"object",properties:{boundTextAllowedPattern:{type:"string"},checkId:{type:"boolean",default:K.checkId},checkText:{type:"boolean",default:K.checkText},checkAttributes:{type:"boolean",default:K.checkAttributes},ignoreAttributes:{type:"array",items:{type:"string"},default:[...z]},ignoreTags:{type:"array",items:{type:"string"}}},additionalProperties:!1}],messages:{i18nAttribute:"Attribute '{{attributeName}}' has no corresponding i18n attribute. See more at https://angular.io/guide/i18n#translate-attributes",i18nId:"Missing custom message identifier. See more at https://angular.io/guide/i18n#use-a-custom-id-with-a-description",i18nIdOnAttribute:'Missing custom message identifier on attribute "{{attributeName}}". See more at https://angular.io/guide/i18n#use-a-custom-id-with-a-description',i18nSuggestIgnore:'Add the attribute name "{{attributeName}}" to the `ignoreAttributes` option in the eslint config',i18nText:"Each element containing text node should have an i18n attribute. See more at https://angular.io/guide/i18n"}},defaultOptions:[K],create(e,[{boundTextAllowedPattern:t,checkAttributes:a,checkId:r,checkText:o,ignoreAttributes:s,ignoreTags:i}]){const c=u(e),l=e.getSourceCode(),m=RegExp(null!=t?t:_),p=new Set([...z,...null!=s?s:[]]),d=new Set(i);function g(e){return e.customId}function f(e){return e instanceof n.TmplAstText&&/\S/.test(e.value)||e instanceof n.TmplAstBoundText&&e.value instanceof n.ASTWithSource&&e.value.ast instanceof n.Interpolation&&m.test(e.value.ast.strings.join("").trim())}function y(e,t,n){return p.has(t)||p.has(`${e}[${t}]`)||0===n.trim().length||"true"===n||"false"===n}function b({attributes:t,children:s,i18n:i,parent:u,sourceSpan:m},p){const d=c.convertNodeSourceSpanToLoc(m);for(const{i18n:n,name:o,value:s}of t)n?r&&!g(n)&&e.report({messageId:"i18nIdOnAttribute",loc:d,data:{attributeName:o}}):a&&y(p,o,s)||e.report({messageId:"i18nAttribute",loc:d,data:{attributeName:o},fix:e=>{const t=l.getIndexFromLoc(d.start)+1+p.length;return e.replaceTextRange([t,t],` i18n-${o}`)},suggest:[{messageId:"i18nSuggestIgnore",data:{attributeName:o},fix:e=>e.insertTextBeforeRange([0,0],"")}]});i?r&&!function(e,t){return!((e instanceof n.TmplAstElement||e instanceof n.TmplAstTemplate)&&(null==e||!e.i18n)&&!g(t))}(u,i)&&e.report({messageId:"i18nId",loc:d}):o&&s.some(f)&&e.report({messageId:"i18nText",loc:d})}return{Element(e){d.has(e.name)||b(e,e.name)},Template(e){b(e,e.tagName)}}}}),Y=l({name:"mouse-events-have-key-events",meta:{type:"suggestion",docs:{description:"Ensures that the Mouse Events mouseover and mouseout are accompanied with Key Events focus and blur.",category:"Best Practices",recommended:!1},schema:[],messages:{mouseOverEventHasFocusEvent:"mouseover must be accompanied by focus event for accessibility.",mouseOutEventHasBlurEvent:"mouseout must be accompanied by blur event for accessibility"}},defaultOptions:[],create(e){const t=u(e);return{Element(n){let a=!1,r=!1,o=!1,s=!1;for(const e of n.outputs)a="mouseover"===e.name,r="mouseout"===e.name,o="focus"===e.name,s="blur"===e.name;if(!a&&!r)return;const i=t.convertNodeSourceSpanToLoc(n.sourceSpan);a&&!o&&e.report({loc:i,messageId:"mouseOverEventHasFocusEvent"}),r&&!s&&e.report({loc:i,messageId:"mouseOutEventHasBlurEvent"})}}}}),J=l({name:"no-any",meta:{type:"suggestion",docs:{description:'The use of "$any" nullifies the compile-time benefits of the Angular\'s type system.',category:"Best Practices",recommended:!1},schema:[],messages:{noAny:'Avoid using "$any" in templates'}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();return{'MethodCall[name="$any"][receiver.expression=undefined][receiver.name=undefined]'({sourceSpan:{end:n,start:a}}){e.report({messageId:"noAny",loc:{start:t.getLocFromIndex(a),end:t.getLocFromIndex(n)}})}}}}),G=l({name:"no-autofocus",meta:{type:"suggestion",docs:{description:"Ensure that autofocus attribute is not used",category:"Best Practices",recommended:!1},schema:[],messages:{noAutofocus:"autofocus attribute should not be used, as it reduces usability and accessibility for users"}},defaultOptions:[],create(e){const t=u(e);return{'TextAttribute[name="autofocus"], BoundAttribute[name="autofocus"]'(n){const a=t.convertNodeSourceSpanToLoc(n.sourceSpan);e.report({loc:a,messageId:"noAutofocus"})}}}});function Q({type:e}){return"Program"===e}var Z=l({name:"no-call-expression",meta:{type:"suggestion",docs:{description:"Disallows calling expressions in templates, except for output handlers",category:"Best Practices",recommended:!1},schema:[],messages:{noCallExpression:"Avoid calling expressions in templates"}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();function a({sourceSpan:{end:n,start:a}}){e.report({messageId:"noCallExpression",loc:{start:t.getLocFromIndex(a),end:t.getLocFromIndex(n)}})}return{'Conditional, MethodCall[name!="$any"], SafeMethodCall'(e){if(function({parent:e},t){for(;e&&!Q(e);){if(t(e))return e;e=e.parent}return null}(e,ee))return;if(!(e instanceof n.Conditional))return void a(e);const{falseExp:t,trueExp:r}=e;te(t)&&a(t),te(r)&&a(r)}}}});function ee(e){return e instanceof n.TmplAstBoundEvent}function te(e){return e instanceof n.MethodCall||e instanceof n.SafeMethodCall}var ne=l({name:"no-distracting-elements",meta:{type:"suggestion",docs:{description:"Enforces that no distracting elements are used",category:"Best Practices",recommended:!1},schema:[],messages:{noDistractingElements:"Do not use <{{element}}> elements as they can create visual accessibility issues and are deprecated"}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(blink|marquee)$/]"({name:n,sourceSpan:a}){const r=t.convertNodeSourceSpanToLoc(a);e.report({loc:r,messageId:"noDistractingElements",data:{element:n}})}}}}),ae=l({name:"no-duplicate-attributes",meta:{type:"problem",docs:{description:"Ensures that there are no duplicate input properties or output event listeners",category:"Possible Errors",recommended:!1},schema:[{type:"object",properties:{allowTwoWayDataBinding:{type:"boolean"}},additionalProperties:!1}],messages:{noDuplicateAttributes:'Duplicate attribute "{{attributeName}}"'}},defaultOptions:[{allowTwoWayDataBinding:!0}],create(e,[{allowTwoWayDataBinding:t}]){const n=u(e);return{Element({inputs:a,outputs:r,attributes:o}){[...oe([...a,...o]),...oe(t?r.filter(e=>!a.some(t=>t.sourceSpan.start===e.sourceSpan.start&&t.sourceSpan.end===e.sourceSpan.end)):r)].forEach(t=>{const a=n.convertNodeSourceSpanToLoc(t.sourceSpan);e.report({messageId:"noDuplicateAttributes",loc:a,data:{attributeName:re(t)}})})}}}});function re(e){if("type"in e)if("BoundAttribute"===e.type)switch(e.__originalType){case 2:return`class.${e.name}`;case 3:return`style.${e.name}${e.unit?"."+e.unit:""}`;case 4:return`@${e.name}`}else if("BoundEvent"===e.type){if(1===e.__originalType)return`@${e.name}${e.phase?"."+e.phase:""}`;if(e.target)return`${e.target}:${e.name}`}return e.name}function oe(e){return e.filter(t=>e.some(e=>e!==t&&re(e)===re(t)))}var se=l({name:"no-negated-async",meta:{type:"suggestion",docs:{description:"Ensures that strict equality is used when evaluating negations on async pipe output",category:"Best Practices",recommended:"error"},schema:[],messages:{noNegatedAsync:"Async pipes should not be negated. Use (observable | async) === (false || null || undefined) to check its value instead",noLooseEquality:"Async pipes must use strict equality `===` when comparing with `false`"}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();return{"PrefixNot > BindingPipe[name=async]"({parent:n}){e.report({messageId:"noNegatedAsync",loc:{start:t.getLocFromIndex(n.sourceSpan.start),end:t.getLocFromIndex(n.sourceSpan.end)}})},'Binary[operation="=="] > BindingPipe[name=async]'({parent:n}){e.report({messageId:"noLooseEquality",loc:{start:t.getLocFromIndex(n.sourceSpan.start),end:t.getLocFromIndex(n.sourceSpan.end)}})}}}}),ie=l({name:"no-positive-tabindex",meta:{type:"suggestion",docs:{description:"Ensures that the tabindex attribute is not positive",category:"Best Practices",recommended:!1},schema:[],messages:{noPositiveTabindex:"tabindex attribute cannot be positive"}},defaultOptions:[],create(e){const t=u(e);return{'BoundAttribute[name="tabindex"][value.ast.value>0], TextAttribute[name="tabindex"][value>0]'({sourceSpan:n}){const a=t.convertNodeSourceSpanToLoc(n);e.report({loc:a,messageId:"noPositiveTabindex"})}}}});function ce(){return(ce=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e}).apply(this,arguments)}var le=l({name:"use-track-by-function",meta:{type:"suggestion",docs:{description:"Ensures trackBy function is used.",category:"Best Practices",recommended:!1},schema:[],messages:{useTrackByFunction:"Missing trackBy function in ngFor directive"}},defaultOptions:[],create(e){const t=u(e);return{'BoundAttribute.inputs[name="ngForOf"]'(n){if(n.parent.inputs.some(e=>"BoundAttribute"===e.type&&"ngForTrackBy"===e.name))return;const a=t.convertNodeSourceSpanToLoc(n.sourceSpan);e.report({messageId:"useTrackByFunction",loc:a})},'BoundAttribute.templateAttrs[name="ngForOf"]'(n){const a=n.parent.templateAttrs;if(a.some(e=>"BoundAttribute"===e.type&&"ngForTrackBy"===e.name))return;const r=t.convertNodeSourceSpanToLoc(a[0].sourceSpan).start,o=t.convertNodeSourceSpanToLoc(a[a.length-1].sourceSpan).end,s={start:ce({},r,{column:r.column-1}),end:ce({},o,{column:o.column+1})};e.report({messageId:"useTrackByFunction",loc:s})}}}});module.exports={configs:{all:{extends:"./configs/base.json",rules:{"@angular-eslint/template/accessibility-alt-text":"error","@angular-eslint/template/accessibility-elements-content":"error","@angular-eslint/template/accessibility-label-for":"error","@angular-eslint/template/accessibility-table-scope":"error","@angular-eslint/template/accessibility-valid-aria":"error","@angular-eslint/template/banana-in-box":"error","@angular-eslint/template/click-events-have-key-events":"error","@angular-eslint/template/conditional-complexity":"error","@angular-eslint/template/cyclomatic-complexity":"error","@angular-eslint/template/i18n":"error","@angular-eslint/template/mouse-events-have-key-events":"error","@angular-eslint/template/no-any":"error","@angular-eslint/template/no-autofocus":"error","@angular-eslint/template/no-call-expression":"error","@angular-eslint/template/no-distracting-elements":"error","@angular-eslint/template/no-duplicate-attributes":"error","@angular-eslint/template/no-negated-async":"error","@angular-eslint/template/no-positive-tabindex":"error","@angular-eslint/template/use-track-by-function":"error"}},base:{parser:"@angular-eslint/template-parser",plugins:["@angular-eslint/template"]},recommended:{extends:"./configs/base.json",rules:{"@angular-eslint/template/banana-in-box":"error","@angular-eslint/template/no-negated-async":"error"}},"process-inline-templates":{parser:"@typescript-eslint/parser",parserOptions:{ecmaVersion:2020,sourceType:"module"},plugins:["@angular-eslint/template"],processor:"@angular-eslint/template/extract-inline-html"}},processors:c,rules:{"accessibility-alt-text":g,"accessibility-elements-content":S,"accessibility-label-for":C,"accessibility-table-scope":k,"accessibility-valid-aria":B,"banana-in-box":P,"conditional-complexity":M,"click-events-have-key-events":R,"cyclomatic-complexity":V,i18n:X,"mouse-events-have-key-events":Y,"no-any":J,"no-autofocus":G,"no-call-expression":Z,"no-distracting-elements":ne,"no-duplicate-attributes":ae,"no-negated-async":se,"no-positive-tabindex":ie,"use-track-by-function":le}};
1
+ var e=require("typescript"),t=require("@typescript-eslint/experimental-utils"),n=require("@angular/compiler"),a=require("aria-query");function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s=o(e);function r(e,t){const n=(e=e.replace(/\r\n/g,"\n")).indexOf(t);return[n,n+t.length]}const i=new Map;var c={"extract-inline-html":{preprocess:function(e,t){const n=[e];if(!function(e,t){return!![".component.ts",".page.ts",".dialog.ts",".modal.ts",".popover.ts",".bottomsheet.ts",".snackbar.ts"].some(e=>t.endsWith(e))||!(!e.includes("Component")||!e.includes("@angular/core"))}(e,t))return n;try{const a=s.default.createSourceFile(t,e,s.default.ScriptTarget.Latest,!0),o=a.statements.filter(e=>s.default.isClassDeclaration(e));if(!o||!o.length)return n;const c=[];for(const e of o)if(e.decorators)for(const t of e.decorators)s.default.isCallExpression(t.expression)&&s.default.isIdentifier(t.expression.expression)&&"Component"===t.expression.expression.text&&c.push(t);if(!c||!c.length)return n;const l=[e];let u=0;for(const t of c){if(!s.default.isDecorator(t)||!s.default.isCallExpression(t.expression)||1!==t.expression.arguments.length)continue;const n=t.expression.arguments[0];if(!s.default.isObjectLiteralExpression(n))continue;const o=n.properties.find(e=>e&&e.name&&"template"===e.name.getText());if(n.properties.find(e=>e&&e.name&&"templateUrl"===e.name.getText())||!o)continue;if(!s.default.isPropertyAssignment(o)||!s.default.isStringLiteralLike(o.initializer))continue;const c=o.initializer.text,m=r(e,c),p=`inline-template-${++u}.component.html`;i.set(p,{range:m,lineAndCharacter:{start:a.getLineAndCharacterOfPosition(m[0]),end:a.getLineAndCharacterOfPosition(m[1])}}),l.push({text:c,filename:p})}return l}catch(e){return console.log(e),console.error("preprocess: ERROR could not parse @Component() metadata",t),n}},postprocess:function(e,t){const n=e[0];if(1===e.length)return n;const a=e.slice(1);return[...n,...[].concat(...a.map((e,t)=>{const n=`inline-template-${++t}.component.html`,a=i.get(n);return a?e.map(e=>{if(e.line=e.line+a.lineAndCharacter.start.line,e.endLine=e.endLine+a.lineAndCharacter.start.line,e.fix){const t=a.range[0];e.fix.range=[t+e.fix.range[0],t+e.fix.range[1]]}return e}):[]}))]},supportsAutofix:!0}};const l=t.ESLintUtils.RuleCreator(e=>"https://github.com/angular-eslint/angular-eslint");function u(e){return m(e),e.parserServices}function m(e){var t,n;if(null==(t=e.parserServices)||!t.convertNodeSourceSpanToLoc||null==(n=e.parserServices)||!n.convertElementSourceSpanToLoc)throw new Error("You have used a rule which requires '@angular-eslint/template-parser' to be used as the 'parser' in your ESLint config.")}const p=Symbol("PROPERTY");function d(e,t){const a=e.attributes.find(e=>e.name===t);if(a)return a.value;const o=e.inputs.find(e=>e.name===t);return o&&o.value instanceof n.ASTWithSource?o.value.ast instanceof n.LiteralPrimitive?o.value.ast.value:p:null}var g=l({name:"accessibility-alt-text",meta:{type:"suggestion",docs:{description:"Enforces alternate text for elements which require the alt, aria-label, aria-labelledby attributes.",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityAltText:"<{{element}}/> element must have a text alternative."}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(img|area|object|input)$/]"(n){if(!function(e){return"img"===e.name?function(e){return e.attributes.some(({name:e})=>b(e))||e.inputs.some(({name:e})=>b(e))}(e):"object"===e.name?function(e){let t=!1,n=!1;for(const a of e.attributes)t="title"===a.name,n=y(a.name);if(t||n)return!0;let a=!1,o=!1;for(const t of e.inputs)a="title"===t.name,o=y(t.name);return!(!a&&!o)||e.children.length>0&&!!e.children[0].value}(e):"area"===e.name?f(e):function(e){return"image"!==d(e,"type")||f(e)}(e)}(n)){const a=t.convertElementSourceSpanToLoc(e,n);e.report({loc:a,messageId:"accessibilityAltText",data:{element:n.name}})}}}}});function f(e){let t=!1,n=!1;for(const a of e.attributes)t=b(a.name),n=y(a.name);if(t||n)return!0;let a=!1,o=!1;for(const t of e.inputs)a=b(t.name),o=y(t.name);return a||o}function y(e){return"aria-label"===e||"aria-labelledby"===e}function b(e){return"alt"===e}function h(e){if("string"==typeof e){if("true"===e)return!0;if("false"===e)return!1}return e}function x(e){if("INPUT"===e.name.toUpperCase()){const t=d(e,"type");if("string"==typeof t&&"HIDDEN"===t.toUpperCase())return!0}const t=h(d(e,"aria-hidden"));return""===t||t===p||!0===t}const v=new Set(["aria-label","innerHtml","innerHTML","innerText","outerHTML","title"]);var S=l({name:"accessibility-elements-content",meta:{type:"suggestion",docs:{description:"Ensures that the heading, anchor and button elements have content in it",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityElementsContent:"<{{element}}> should have content"}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(a|button|h1|h2|h3|h4|h5|h6)$/][children.length=0]"(n){if(x(n))return;const{attributes:a,inputs:o,name:s,sourceSpan:r}=n;if([...a,...o].map(({name:e})=>e).some(e=>v.has(e)))return;const i=t.convertNodeSourceSpanToLoc(r);e.report({loc:i,messageId:"accessibilityElementsContent",data:{element:s}})}}}});function A(e,t){return function e({children:a}){return a.some(a=>a instanceof n.TmplAstElement&&(a.name===t||e(a)))}(e)}const T={items:{type:"string"},type:"array",uniqueItems:!0},C=["button","input","meter","output","progress","select","textarea"],E=["for","htmlFor"],I=["label"];var B=l({name:"accessibility-label-for",meta:{deprecated:!0,replacedBy:["accessibility-label-has-associated-control"],type:"suggestion",docs:{description:"Ensures that a label element/component is associated with a form element",category:"Best Practices",recommended:!1},schema:[{additionalProperties:!1,properties:{controlComponents:T,labelAttributes:T,labelComponents:T},type:"object"}],messages:{accessibilityLabelFor:"A label element/component must be associated with a form element"}},defaultOptions:[{controlComponents:C,labelAttributes:E,labelComponents:I}],create(e,[t]){const n=u(e),{controlComponents:a,labelAttributes:o,labelComponents:s}=function({controlComponents:e,labelAttributes:t,labelComponents:n}){return{controlComponents:new Set([...C,...null!=e?e:[]]),labelAttributes:new Set([...E,...null!=t?t:[]]),labelComponents:new Set([...I,...null!=n?n:[]])}}(t);var r;return{[`Element[name=${r=[...s],RegExp(`^(${r.join("|")})$`)}]`](t){const s=new Set([...t.attributes,...t.inputs].map(({name:e})=>e));if([...o].some(e=>s.has(e))||function(e,t){return Boolean([...e].some(e=>A(t,e)))}(a,t))return;const r=n.convertNodeSourceSpanToLoc(t.sourceSpan);e.report({loc:r,messageId:"accessibilityLabelFor"})}}}});const k="accessibility-label-has-associated-control",w=["input","meter","output","progress","select","textarea"],L=[{inputs:["for","htmlFor"],selector:"label"}];var N=l({name:k,meta:{type:"suggestion",docs:{description:"Ensures that a label element/component is associated with a form element",category:"Best Practices",recommended:!1},schema:[{additionalProperties:!1,properties:{controlComponents:{items:{type:"string"},type:"array",uniqueItems:!0},labelComponents:{items:{required:["selector"],type:"object"},type:"array",uniqueItems:!0}},type:"object"}],messages:{accessibilityLabelHasAssociatedControl:"A label component must be associated with a form element"}},defaultOptions:[{controlComponents:w,labelComponents:L}],create(e,[{controlComponents:t,labelComponents:n}]){const a=u(e),o=new Set([...w,...null!=t?t:[]]),s=[...L,...null!=n?n:[]],r=s.map(({selector:e})=>e);return{[`Element[name=${RegExp(`^(${r.join("|")})$`)}]`](t){var n;const r=s.find(({selector:e})=>e===t.name);if(!r)return;const i=new Set([...t.attributes,...t.inputs].map(({name:e})=>e));if((null==(n=r.inputs)?void 0:n.some(e=>i.has(e)))||function(e,t){return Boolean([...e].some(e=>A(t,e)))}(o,t))return;const c=a.convertNodeSourceSpanToLoc(t.sourceSpan);e.report({loc:c,messageId:"accessibilityLabelHasAssociatedControl"})}}}}),P=l({name:"accessibility-table-scope",meta:{type:"suggestion",docs:{description:"Ensures that scope is not used on any element except <th>",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityTableScope:"Scope attribute can only be on <th> element"}},defaultOptions:[],create(e){const t=u(e);return{'Element[name!="th"] > :matches(BoundAttribute[name="scope"], TextAttribute[name="scope"])'({sourceSpan:n}){const a=t.convertNodeSourceSpanToLoc(n);e.report({loc:a,messageId:"accessibilityTableScope"})}}}}),O=l({name:"accessibility-valid-aria",meta:{type:"suggestion",docs:{description:"Ensures that correct ARIA attributes and respective values are used",category:"Best Practices",recommended:!1},schema:[],messages:{accessibilityValidAria:"The `{{attribute}}` is an invalid ARIA attribute",accessibilityValidAriaValue:"The `{{attribute}}` has an invalid value. Check the valid values at https://raw.githack.com/w3c/aria/stable/#roles"}},defaultOptions:[],create(e){const t=u(e);return{"BoundAttribute[name=/^aria-.*/], TextAttribute[name=/^aria-.*/]"(o){const{name:s,sourceSpan:r}=o,i=a.aria.get(s),c=t.convertNodeSourceSpanToLoc(r);if(!i)return void e.report({loc:c,messageId:"accessibilityValidAria",data:{attribute:s}});const l=function(e){return e instanceof n.TmplAstBoundAttribute?e.value.ast:e}(o);(function(e){return!function(e){return e instanceof n.LiteralArray||e instanceof n.LiteralMap}(e)&&!function(e){return e instanceof n.LiteralPrimitive||e instanceof n.TmplAstTextAttribute}(e)})(l)||function({allowundefined:e,type:t,values:n},a){if(e&&$(a))return!0;switch(t){case"boolean":return F(a);case"tristate":return F(a)||$(a);case"id":case"idlist":return!0;case"integer":return o=a,!Number.isNaN(o)&&parseInt(Number(o))==o&&!Number.isNaN(parseInt(o,10));case"number":return function(e){return!Number.isNaN(Number.parseFloat(e))&&Number.isFinite(e)}(a);case"string":return function(e){return"string"==typeof e}(a);case"token":case"tokenlist":{const e=F(a)?JSON.parse(a):a;return Boolean(null==n?void 0:n.includes(e))}}var o}(i,l.value)||e.report({loc:c,messageId:"accessibilityValidAriaValue",data:{attribute:s}})}}}});function F(e){return"boolean"==typeof e||"false"===e||"true"===e}function $(e){return null==e}const j=/\[(.*)\]/;var q=l({name:"banana-in-box",meta:{type:"suggestion",docs:{description:"Ensures that the two-way data binding syntax is correct",category:"Best Practices",recommended:"error"},fixable:"code",schema:[],messages:{bananaInBox:"Invalid binding syntax. Use [(expr)] instead"}},defaultOptions:[],create(e){const t=u(e),n=e.getSourceCode();return{BoundEvent({name:a,sourceSpan:o}){const s=a.match(j);if(!s)return;const[,r]=s,i=`[(${r})]`,c=t.convertNodeSourceSpanToLoc(o),l=n.getIndexFromLoc(c.start);e.report({messageId:"bananaInBox",loc:c,fix:e=>e.replaceTextRange([l,l+a.length+2],i)})}}}});let D=null;function R(){return D||(D=new Set(a.dom.keys()))}const H=new Set(["presentation","none",p]);let M=null,W=null,U=null;var V=l({name:"click-events-have-key-events",meta:{type:"suggestion",docs:{description:"Ensures that the click event is accompanied with at least one key event keyup, keydown or keypress.",category:"Best Practices",recommended:!1},schema:[],messages:{clickEventsHaveKeyEvents:"click must be accompanied by either keyup, keydown or keypress event for accessibility."}},defaultOptions:[],create:e=>({Element(t){if(!R().has(t.name))return;if(function(e){const t=d(e,"role");return null!==t&&H.has(t)}(t)||x(t)||function(e){return R().has(e.name)&&function(e){function t(t){return e.name===t.name&&function(e=[],t){const n=[...t.attributes,...t.inputs];return e.every(e=>n.some(n=>"a"===t.name&&"routerLink"===n.name||e.name===n.name&&(!e.value||e.value===h(d(t,e.name)))))}(t.attributes,e)}return!!function(){if(null===M){const e=[...a.roles.keys()],t=[...a.elementRoles],n=new Set(e.filter(e=>{const t=a.roles.get(e);return!t.abstract&&"progressbar"!==e&&t.superClass.some(e=>e.includes("widget"))}).concat("toolbar"));M=t.reduce((e,[t,a])=>([...a].some(e=>n.has(e))&&e.push(t),e),[])}return M}().some(t)||!function(){if(null===W){const e=[...a.roles.keys()],t=[...a.elementRoles],n=new Set(e.filter(e=>{const t=a.roles.get(e);return!t.abstract&&"toolbar"!==e&&!t.superClass.some(e=>e.includes("widget"))}).concat("progressbar"));W=t.reduce((e,[t,a])=>([...a].every(e=>n.has(e))&&e.push(t),e),[])}return W}().some(t)&&!!function(){if(null===U){const{AXObjects:e,elementAXObjects:t}=require("axobject-query"),n=new Set(Array.from(e.keys()).filter(t=>"widget"===e.get(t).type));U=[...t].reduce((e,[t,a])=>([...a].every(e=>n.has(e))&&e.push(t),e),[])}return U}().some(t)}(e)}(t))return;let n=!1,o=!1;for(const e of t.outputs)n="click"===e.name,o=e.name.startsWith("keyup")||e.name.startsWith("keydown")||e.name.startsWith("keypress");if(!n||o)return;const s=u(e).convertNodeSourceSpanToLoc(t.sourceSpan);e.report({loc:s,messageId:"clickEventsHaveKeyEvents"})}})}),_=l({name:"conditional-complexity",meta:{type:"suggestion",docs:{description:"The conditional complexity should not exceed a rational limit",category:"Best Practices",recommended:!1},schema:[{type:"object",properties:{maxComplexity:{minimum:1,type:"number"}},additionalProperties:!1}],messages:{"conditionalСomplexity":'The conditional complexity "{{totalComplexity}}" exceeds the defined limit "{{maxComplexity}}"'}},defaultOptions:[{maxComplexity:5}],create(e,[{maxComplexity:t}]){m(e);const a=e.getSourceCode();return{BoundAttribute(o){if(!o.value.source)return;const s=X(z((K||(K=new n.Parser(new n.Lexer))).parseBinding(o.value.source,"",0).ast));if(s<=t)return;const{sourceSpan:{start:r,end:i}}=o.value;e.report({loc:{start:a.getLocFromIndex(r),end:a.getLocFromIndex(i)},messageId:"conditionalСomplexity",data:{maxComplexity:t,totalComplexity:s}})},Interpolation({expressions:n}){for(const o of n){const n=X(o);if(n<=t)continue;const{sourceSpan:{start:s,end:r}}=o;e.report({loc:{start:a.getLocFromIndex(s),end:a.getLocFromIndex(r)},messageId:"conditionalСomplexity",data:{maxComplexity:t,totalComplexity:n}})}}}}});function z(e){return e instanceof n.BindingPipe?e.exp:e}let K=null;function X(e){const t=z(e);if(!(t instanceof n.Binary||t instanceof n.Conditional))return 0;let a=1;return t instanceof n.Binary&&(t.left instanceof n.Binary&&(a+=X(t.left)),t.right instanceof n.Binary&&(a+=X(t.right))),t instanceof n.Conditional&&(a+=X(t.condition)+X(t.trueExp)+X(t.falseExp)),a}var Y=l({name:"cyclomatic-complexity",meta:{type:"suggestion",docs:{description:"Checks cyclomatic complexity against a specified limit. It is a quantitative measure of the number of linearly independent paths through a program's source code",category:"Best Practices",recommended:!1},schema:[{type:"object",properties:{maxComplexity:{type:"number",minimum:1}},additionalProperties:!1}],messages:{cyclomaticComplexity:'The cyclomatic complexity "{{totalComplexity}}" exceeds the defined limit "{{maxComplexity}}"'}},defaultOptions:[{maxComplexity:5}],create(e,[{maxComplexity:t}]){let n=0;const a=u(e);return{'BoundAttribute[name=/^(ngForOf|ngIf|ngSwitchCase)$/], TextAttribute[name="ngSwitchDefault"]'({sourceSpan:o}){if(n+=1,n<=t)return;const s=a.convertNodeSourceSpanToLoc(o);e.report({messageId:"cyclomaticComplexity",loc:s,data:{maxComplexity:t,totalComplexity:n}})}}}});const J=/[a-z]/i,G=new Set(["charset","class","color","colspan","fill","formControlName","height","href","id","lang","src","stroke","stroke-width","style","svgIcon","tabindex","target","type","viewBox","width","xmlns"]),Q={checkAttributes:!0,checkId:!0,checkText:!0,ignoreAttributes:[...G]};var Z=l({name:"i18n",meta:{type:"suggestion",docs:{description:"Helps to ensure following best practices for i18n. Checks for missing i18n attributes on elements and non-ignored attributes containing text. Can also highlight tags that do not use custom ID (@@) feature. ",category:"Best Practices",recommended:!1,suggestion:!0},fixable:"code",schema:[{type:"object",properties:{boundTextAllowedPattern:{type:"string"},checkId:{type:"boolean",default:Q.checkId},checkText:{type:"boolean",default:Q.checkText},checkAttributes:{type:"boolean",default:Q.checkAttributes},ignoreAttributes:{type:"array",items:{type:"string"},default:[...G]},ignoreTags:{type:"array",items:{type:"string"}}},additionalProperties:!1}],messages:{i18nAttribute:"Attribute '{{attributeName}}' has no corresponding i18n attribute. See more at https://angular.io/guide/i18n#translate-attributes",i18nId:"Missing custom message identifier. See more at https://angular.io/guide/i18n#use-a-custom-id-with-a-description",i18nIdOnAttribute:'Missing custom message identifier on attribute "{{attributeName}}". See more at https://angular.io/guide/i18n#use-a-custom-id-with-a-description',i18nSuggestIgnore:'Add the attribute name "{{attributeName}}" to the `ignoreAttributes` option in the eslint config',i18nText:"Each element containing text node should have an i18n attribute. See more at https://angular.io/guide/i18n"}},defaultOptions:[Q],create(e,[{boundTextAllowedPattern:t,checkAttributes:a,checkId:o,checkText:s,ignoreAttributes:r,ignoreTags:i}]){const c=u(e),l=e.getSourceCode(),m=RegExp(null!=t?t:J),p=new Set([...G,...null!=r?r:[]]),d=new Set(i);function g(e){return e.customId}function f(e){return e instanceof n.TmplAstText&&/\S/.test(e.value)||e instanceof n.TmplAstBoundText&&e.value instanceof n.ASTWithSource&&e.value.ast instanceof n.Interpolation&&m.test(e.value.ast.strings.join("").trim())}function y(e,t,n){return p.has(t)||p.has(`${e}[${t}]`)||0===n.trim().length||"true"===n||"false"===n}function b({attributes:t,children:r,i18n:i,parent:u,sourceSpan:m},p){const d=c.convertNodeSourceSpanToLoc(m);for(const{i18n:n,name:s,value:r}of t)n?o&&!g(n)&&e.report({messageId:"i18nIdOnAttribute",loc:d,data:{attributeName:s}}):a&&y(p,s,r)||e.report({messageId:"i18nAttribute",loc:d,data:{attributeName:s},fix:e=>{const t=l.getIndexFromLoc(d.start)+1+p.length;return e.replaceTextRange([t,t],` i18n-${s}`)},suggest:[{messageId:"i18nSuggestIgnore",data:{attributeName:s},fix:e=>e.insertTextBeforeRange([0,0],"")}]});i?o&&!function(e,t){return!((e instanceof n.TmplAstElement||e instanceof n.TmplAstTemplate)&&(null==e||!e.i18n)&&!g(t))}(u,i)&&e.report({messageId:"i18nId",loc:d}):s&&r.some(f)&&e.report({messageId:"i18nText",loc:d})}return{Element(e){d.has(e.name)||b(e,e.name)},Template(e){b(e,e.tagName)}}}}),ee=l({name:"mouse-events-have-key-events",meta:{type:"suggestion",docs:{description:"Ensures that the Mouse Events mouseover and mouseout are accompanied with Key Events focus and blur.",category:"Best Practices",recommended:!1},schema:[],messages:{mouseOverEventHasFocusEvent:"mouseover must be accompanied by focus event for accessibility.",mouseOutEventHasBlurEvent:"mouseout must be accompanied by blur event for accessibility"}},defaultOptions:[],create(e){const t=u(e);return{Element(n){let a=!1,o=!1,s=!1,r=!1;for(const e of n.outputs)a="mouseover"===e.name,o="mouseout"===e.name,s="focus"===e.name,r="blur"===e.name;if(!a&&!o)return;const i=t.convertNodeSourceSpanToLoc(n.sourceSpan);a&&!s&&e.report({loc:i,messageId:"mouseOverEventHasFocusEvent"}),o&&!r&&e.report({loc:i,messageId:"mouseOutEventHasBlurEvent"})}}}}),te=l({name:"no-any",meta:{type:"suggestion",docs:{description:'The use of "$any" nullifies the compile-time benefits of the Angular\'s type system.',category:"Best Practices",recommended:!1},schema:[],messages:{noAny:'Avoid using "$any" in templates'}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();return{'MethodCall[name="$any"][receiver.expression=undefined][receiver.name=undefined]'({sourceSpan:{end:n,start:a}}){e.report({messageId:"noAny",loc:{start:t.getLocFromIndex(a),end:t.getLocFromIndex(n)}})}}}}),ne=l({name:"no-autofocus",meta:{type:"suggestion",docs:{description:"Ensure that autofocus attribute is not used",category:"Best Practices",recommended:!1},schema:[],messages:{noAutofocus:"autofocus attribute should not be used, as it reduces usability and accessibility for users"}},defaultOptions:[],create(e){const t=u(e);return{'TextAttribute[name="autofocus"], BoundAttribute[name="autofocus"]'(n){const a=t.convertNodeSourceSpanToLoc(n.sourceSpan);e.report({loc:a,messageId:"noAutofocus"})}}}});function ae({type:e}){return"Program"===e}var oe=l({name:"no-call-expression",meta:{type:"suggestion",docs:{description:"Disallows calling expressions in templates, except for output handlers",category:"Best Practices",recommended:!1},schema:[],messages:{noCallExpression:"Avoid calling expressions in templates"}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();function a({sourceSpan:{end:n,start:a}}){e.report({messageId:"noCallExpression",loc:{start:t.getLocFromIndex(a),end:t.getLocFromIndex(n)}})}return{'Conditional, MethodCall[name!="$any"], SafeMethodCall'(e){if(function({parent:e},t){for(;e&&!ae(e);){if(t(e))return e;e=e.parent}return null}(e,se))return;if(!(e instanceof n.Conditional))return void a(e);const{falseExp:t,trueExp:o}=e;re(t)&&a(t),re(o)&&a(o)}}}});function se(e){return e instanceof n.TmplAstBoundEvent}function re(e){return e instanceof n.MethodCall||e instanceof n.SafeMethodCall}var ie=l({name:"no-distracting-elements",meta:{type:"suggestion",docs:{description:"Enforces that no distracting elements are used",category:"Best Practices",recommended:!1},schema:[],messages:{noDistractingElements:"Do not use <{{element}}> elements as they can create visual accessibility issues and are deprecated"}},defaultOptions:[],create(e){const t=u(e);return{"Element[name=/^(blink|marquee)$/]"({name:n,sourceSpan:a}){const o=t.convertNodeSourceSpanToLoc(a);e.report({loc:o,messageId:"noDistractingElements",data:{element:n}})}}}}),ce=l({name:"no-duplicate-attributes",meta:{type:"problem",docs:{description:"Ensures that there are no duplicate input properties or output event listeners",category:"Possible Errors",recommended:!1},schema:[{type:"object",properties:{allowTwoWayDataBinding:{type:"boolean"}},additionalProperties:!1}],messages:{noDuplicateAttributes:'Duplicate attribute "{{attributeName}}"'}},defaultOptions:[{allowTwoWayDataBinding:!0}],create(e,[{allowTwoWayDataBinding:t}]){const n=u(e);return{Element({inputs:a,outputs:o,attributes:s}){[...ue([...a,...s]),...ue(t?o.filter(e=>!a.some(t=>t.sourceSpan.start===e.sourceSpan.start&&t.sourceSpan.end===e.sourceSpan.end)):o)].forEach(t=>{const a=n.convertNodeSourceSpanToLoc(t.sourceSpan);e.report({messageId:"noDuplicateAttributes",loc:a,data:{attributeName:le(t)}})})}}}});function le(e){if("type"in e)if("BoundAttribute"===e.type)switch(e.__originalType){case 2:return`class.${e.name}`;case 3:return`style.${e.name}${e.unit?"."+e.unit:""}`;case 4:return`@${e.name}`}else if("BoundEvent"===e.type){if(1===e.__originalType)return`@${e.name}${e.phase?"."+e.phase:""}`;if(e.target)return`${e.target}:${e.name}`}return e.name}function ue(e){return e.filter(t=>e.some(e=>e!==t&&le(e)===le(t)))}var me=l({name:"no-negated-async",meta:{type:"suggestion",docs:{description:"Ensures that strict equality is used when evaluating negations on async pipe output",category:"Best Practices",recommended:"error"},schema:[],messages:{noNegatedAsync:"Async pipes should not be negated. Use (observable | async) === (false || null || undefined) to check its value instead",noLooseEquality:"Async pipes must use strict equality `===` when comparing with `false`"}},defaultOptions:[],create(e){m(e);const t=e.getSourceCode();return{"PrefixNot > BindingPipe[name=async]"({parent:n}){e.report({messageId:"noNegatedAsync",loc:{start:t.getLocFromIndex(n.sourceSpan.start),end:t.getLocFromIndex(n.sourceSpan.end)}})},'Binary[operation="=="] > BindingPipe[name=async]'({parent:n}){e.report({messageId:"noLooseEquality",loc:{start:t.getLocFromIndex(n.sourceSpan.start),end:t.getLocFromIndex(n.sourceSpan.end)}})}}}}),pe=l({name:"no-positive-tabindex",meta:{type:"suggestion",docs:{description:"Ensures that the tabindex attribute is not positive",category:"Best Practices",recommended:!1},schema:[],messages:{noPositiveTabindex:"tabindex attribute cannot be positive"}},defaultOptions:[],create(e){const t=u(e);return{'BoundAttribute[name="tabindex"][value.ast.value>0], TextAttribute[name="tabindex"][value>0]'({sourceSpan:n}){const a=t.convertNodeSourceSpanToLoc(n);e.report({loc:a,messageId:"noPositiveTabindex"})}}}});function de(){return(de=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(e[a]=n[a])}return e}).apply(this,arguments)}var ge=l({name:"use-track-by-function",meta:{type:"suggestion",docs:{description:"Ensures trackBy function is used.",category:"Best Practices",recommended:!1},schema:[],messages:{useTrackByFunction:"Missing trackBy function in ngFor directive"}},defaultOptions:[],create(e){const t=u(e);return{'BoundAttribute.inputs[name="ngForOf"]'(n){if(n.parent.inputs.some(e=>"BoundAttribute"===e.type&&"ngForTrackBy"===e.name))return;const a=t.convertNodeSourceSpanToLoc(n.sourceSpan);e.report({messageId:"useTrackByFunction",loc:a})},'BoundAttribute.templateAttrs[name="ngForOf"]'(n){const a=n.parent.templateAttrs;if(a.some(e=>"BoundAttribute"===e.type&&"ngForTrackBy"===e.name))return;const o=t.convertNodeSourceSpanToLoc(a[0].sourceSpan).start,s=t.convertNodeSourceSpanToLoc(a[a.length-1].sourceSpan).end,r={start:de({},o,{column:o.column-1}),end:de({},s,{column:s.column+1})};e.report({messageId:"useTrackByFunction",loc:r})}}}});module.exports={configs:{all:{extends:"./configs/base.json",rules:{"@angular-eslint/template/accessibility-alt-text":"error","@angular-eslint/template/accessibility-elements-content":"error","@angular-eslint/template/accessibility-label-for":"error","@angular-eslint/template/accessibility-label-has-associated-control":"error","@angular-eslint/template/accessibility-table-scope":"error","@angular-eslint/template/accessibility-valid-aria":"error","@angular-eslint/template/banana-in-box":"error","@angular-eslint/template/click-events-have-key-events":"error","@angular-eslint/template/conditional-complexity":"error","@angular-eslint/template/cyclomatic-complexity":"error","@angular-eslint/template/i18n":"error","@angular-eslint/template/mouse-events-have-key-events":"error","@angular-eslint/template/no-any":"error","@angular-eslint/template/no-autofocus":"error","@angular-eslint/template/no-call-expression":"error","@angular-eslint/template/no-distracting-elements":"error","@angular-eslint/template/no-duplicate-attributes":"error","@angular-eslint/template/no-negated-async":"error","@angular-eslint/template/no-positive-tabindex":"error","@angular-eslint/template/use-track-by-function":"error"}},base:{parser:"@angular-eslint/template-parser",plugins:["@angular-eslint/template"]},recommended:{extends:"./configs/base.json",rules:{"@angular-eslint/template/banana-in-box":"error","@angular-eslint/template/no-negated-async":"error"}},"process-inline-templates":{parser:"@typescript-eslint/parser",parserOptions:{ecmaVersion:2020,sourceType:"module"},plugins:["@angular-eslint/template"],processor:"@angular-eslint/template/extract-inline-html"}},processors:c,rules:{"accessibility-alt-text":g,"accessibility-elements-content":S,"accessibility-label-for":B,[k]:N,"accessibility-table-scope":P,"accessibility-valid-aria":O,"banana-in-box":q,"conditional-complexity":_,"click-events-have-key-events":V,"cyclomatic-complexity":Y,i18n:Z,"mouse-events-have-key-events":ee,"no-any":te,"no-autofocus":ne,"no-call-expression":oe,"no-distracting-elements":ie,"no-duplicate-attributes":ce,"no-negated-async":me,"no-positive-tabindex":pe,"use-track-by-function":ge}};
@@ -0,0 +1,14 @@
1
+ declare type LabelComponent = {
2
+ readonly inputs?: readonly string[];
3
+ readonly selector: string;
4
+ };
5
+ declare type Options = [
6
+ {
7
+ readonly controlComponents?: readonly string[];
8
+ readonly labelComponents?: readonly LabelComponent[];
9
+ }
10
+ ];
11
+ export declare type MessageIds = 'accessibilityLabelHasAssociatedControl';
12
+ export declare const RULE_NAME = "accessibility-label-has-associated-control";
13
+ declare const _default: import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleModule<"accessibilityLabelHasAssociatedControl", Options, import("@typescript-eslint/experimental-utils/dist/ts-eslint/Rule").RuleListener>;
14
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-eslint/eslint-plugin-template",
3
- "version": "4.2.2-alpha.0+1539564",
3
+ "version": "4.3.0",
4
4
  "description": "ESLint plugin for Angular Templates",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "axobject-query": "^2.2.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@angular-eslint/utils": "4.2.1",
32
+ "@angular-eslint/utils": "4.3.0",
33
33
  "@types/aria-query": "^4.2.0"
34
34
  },
35
35
  "peerDependencies": {
@@ -38,5 +38,5 @@
38
38
  "eslint": "*",
39
39
  "typescript": "*"
40
40
  },
41
- "gitHead": "15395649712625567c5dd4157f88a7cf6a3ea6ce"
41
+ "gitHead": "5f4e4a8904de2b2be8db55c4fbc6f3946bfe9185"
42
42
  }