Commit ca7e835d authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): add `isKeywordExtended` method for rule authors

parent b046dc59
Pipeline #146590791 passed with stages
in 9 minutes and 30 seconds
......@@ -5,7 +5,7 @@ import { HtmlElement } from "./dom";
import { Event } from "./event";
import { Parser } from "./parser";
import { Reporter } from "./reporter";
import { Rule, ruleDocumentationUrl } from "./rule";
import { Rule, ruleDocumentationUrl, IncludeExcludeOptions } from "./rule";
import { MetaTable } from "./meta";
interface RuleContext {
......@@ -221,6 +221,47 @@ describe("rule base class", () => {
expect(rule.documentation()).toBeNull();
});
describe("isKeywordIgnored()", () => {
class RuleWithOption extends Rule<void, IncludeExcludeOptions> {
public setup(): void {
/* do nothing */
}
}
let rule: RuleWithOption;
let options: IncludeExcludeOptions;
beforeEach(() => {
options = {
include: null,
exclude: null,
};
rule = new RuleWithOption(options);
});
it('should return true if keyword is not present in "include"', () => {
expect.assertions(2);
options.include = ["foo"];
expect(rule.isKeywordIgnored("foo")).toBeFalsy();
expect(rule.isKeywordIgnored("bar")).toBeTruthy();
});
it('should return true if keyword is present in "exclude"', () => {
expect.assertions(2);
options.exclude = ["foo"];
expect(rule.isKeywordIgnored("foo")).toBeTruthy();
expect(rule.isKeywordIgnored("bar")).toBeFalsy();
});
it('should return true if keyword satisfies both "include" and "exclude"', () => {
expect.assertions(2);
options.include = ["foo", "bar"];
options.exclude = ["bar"];
expect(rule.isKeywordIgnored("foo")).toBeFalsy();
expect(rule.isKeywordIgnored("bar")).toBeTruthy();
});
});
it("getTagsWithProperty() should lookup properties from metadata", () => {
expect.assertions(2);
const spy = jest.spyOn(meta, "getTagsWithProperty");
......
......@@ -27,6 +27,11 @@ export interface RuleDocumentation {
export type RuleConstructor<T, U> = new (options?: any) => Rule<T, U>;
export interface IncludeExcludeOptions {
include: string[] | null;
exclude: string[] | null;
}
export abstract class Rule<ContextType = void, OptionsType = void> {
private reporter: Reporter;
private parser: Parser;
......@@ -82,6 +87,49 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
return this.enabled && this.severity >= Severity.WARN;
}
/**
* Check if keyword is being ignored by the current rule configuration.
*
* This method requires the [[RuleOption]] type to include two properties:
*
* - include: string[] | null
* - exclude: string[] | null
*
* This methods checks if the given keyword is included by "include" but not
* excluded by "exclude". If any property is unset it is skipped by the
* condition. Usually the user would use either one but not both but there is
* no limitation to use both but the keyword must satisfy both conditions. If
* either condition fails `true` is returned.
*
* For instance, given `{ include: ["foo"] }` the keyword `"foo"` would match
* but not `"bar"`.
*
* Similarly, given `{ exclude: ["foo"] }` the keyword `"bar"` would match but
* not `"foo"`.
*
* @param keyword - Keyword to match against `include` and `exclude` options.
* @returns `true` if keyword is not present in `include` or is present in
* `exclude`.
*/
public isKeywordIgnored<T extends IncludeExcludeOptions>(
this: { options: T },
keyword: string
): boolean {
const { include, exclude } = this.options;
/* ignore keyword if not present in "include" */
if (include && !include.includes(keyword)) {
return true;
}
/* ignore keyword if present in "excludes" */
if (exclude && exclude.includes(keyword)) {
return true;
}
return false;
}
/**
* Find all tags which has enabled given property.
*/
......
......@@ -48,7 +48,7 @@ export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
/* ignore tagnames configured to be ignored */
const tagName = event.target.tagName;
if (this.isIgnored(tagName)) {
if (this.isKeywordIgnored(tagName)) {
return;
}
......@@ -63,20 +63,4 @@ export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
);
});
}
private isIgnored(tagName: string): boolean {
const { include, exclude } = this.options;
/* ignore tagnames not present in "include" */
if (include && !include.includes(tagName)) {
return true;
}
/* ignore tagnames present in "excludes" */
if (exclude && exclude.includes(tagName)) {
return true;
}
return false;
}
}
......@@ -57,7 +57,7 @@ export default class PreferButton extends Rule<RuleContext, RuleOptions> {
}
/* ignore types configured to be ignored */
if (this.isIgnored(event.value)) {
if (this.isKeywordIgnored(event.value)) {
return;
}
......@@ -71,20 +71,4 @@ export default class PreferButton extends Rule<RuleContext, RuleOptions> {
this.report(node, message, event.valueLocation, context);
});
}
private isIgnored(type: string): boolean {
const { include, exclude } = this.options;
/* ignore roles not present in "include" */
if (include && !include.includes(type)) {
return true;
}
/* ignore roles present in "excludes" */
if (exclude && exclude.includes(type)) {
return true;
}
return false;
}
}
......@@ -101,7 +101,7 @@ export default class PreferNativeElement extends Rule<
}
private isIgnored(role: string): boolean {
const { mapping, include, exclude } = this.options;
const { mapping } = this.options;
/* ignore roles not mapped to native elements */
const replacement = mapping[role];
......@@ -109,17 +109,7 @@ export default class PreferNativeElement extends Rule<
return true;
}
/* ignore roles not present in "include" */
if (include && !include.includes(role)) {
return true;
}
/* ignore roles present in "excludes" */
if (exclude && exclude.includes(role)) {
return true;
}
return false;
return this.isKeywordIgnored(role);
}
private getLocation(event: AttributeEvent): Location {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment