From 596fa24b9bb6330a52e85429d58978872d7b7d99 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 13 Oct 2019 14:23:39 +0200 Subject: [PATCH] build: add linting for classList usage Once in a while we hit an issue because we use some of the `classList` methods in a way that isn't supported in all browsers (e.g. passing multiple parameters to `add` in #17378). These changes add a simple lint rule to catch these cases earlier. --- .../mdc-button/button-base.ts | 6 ++- tools/tslint-rules/classListSignaturesRule.ts | 46 +++++++++++++++++++ tslint.json | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tools/tslint-rules/classListSignaturesRule.ts diff --git a/src/material-experimental/mdc-button/button-base.ts b/src/material-experimental/mdc-button/button-base.ts index 31f77df01006..6b59908d66a1 100644 --- a/src/material-experimental/mdc-button/button-base.ts +++ b/src/material-experimental/mdc-button/button-base.ts @@ -103,11 +103,15 @@ export class MatButtonBase extends _MatButtonBaseMixin implements CanDisable, Ca @Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) { super(elementRef); + const classList = (elementRef.nativeElement as HTMLElement).classList; + // For each of the variant selectors that is present in the button's host // attributes, add the correct corresponding MDC classes. for (const pair of HOST_SELECTOR_MDC_CLASS_PAIR) { if (this._hasHostAttributes(pair.selector)) { - (elementRef.nativeElement as HTMLElement).classList.add(...pair.mdcClasses); + pair.mdcClasses.forEach(className => { + classList.add(className); + }); } } } diff --git a/tools/tslint-rules/classListSignaturesRule.ts b/tools/tslint-rules/classListSignaturesRule.ts new file mode 100644 index 000000000000..31c22356f557 --- /dev/null +++ b/tools/tslint-rules/classListSignaturesRule.ts @@ -0,0 +1,46 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +/** + * Rule that catches cases where `classList` is used in a way + * that won't work in all browsers that we support. + */ +export class Rule extends Lint.Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program)); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + visitPropertyAccessExpression(propertyAccess: ts.PropertyAccessExpression) { + const parent = propertyAccess.parent; + + // We only care about property accesses inside of calls. + if (!ts.isCallExpression(parent)) { + return; + } + + // We only care about these method names. + const name = propertyAccess.name.text; + if (name !== 'add' && name !== 'remove' && name !== 'toggle' && name !== 'replace') { + return; + } + + const symbol = this.getTypeChecker().getTypeAtLocation(propertyAccess.expression).symbol; + + if (symbol && symbol.name === 'DOMTokenList') { + const args = parent.arguments; + + if (name === 'replace') { + this.addFailureAtNode(propertyAccess, + 'This method is not supported in iOS Safari. Use `add` and `remove` instead.'); + } else if (args.length > 1 || (args.length === 1 && ts.isSpreadElement(args[0]))) { + this.addFailureAtNode(propertyAccess, + 'Passing in multiple arguments into this method is not supported in some browsers. ' + + 'Use the single argument signature instead.'); + } + } + + super.visitPropertyAccessExpression(propertyAccess); + } +} diff --git a/tslint.json b/tslint.json index 98efb1035c34..7a3cca1bd889 100644 --- a/tslint.json +++ b/tslint.json @@ -107,6 +107,7 @@ "rxjs-imports": true, "require-breaking-change-version": true, "static-query": true, + "class-list-signatures": true, "no-host-decorator-in-concrete": [ true, "HostBinding",