Skip to content

Commit

Permalink
No output named after standard event (#474)
Browse files Browse the repository at this point in the history
* feat(no-output-named-after-standard-event-rule): Initial version

* feat(no-output-named-after-standard-event-rule): Add rationale

* feat(no-output-named-after-standard-event-rule): Use hash map instead of array for constant time check
  • Loading branch information
gbilodeau authored and mgechev committed Dec 18, 2017
1 parent 582d4c4 commit ae3f07b
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 14 deletions.
55 changes: 41 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -11,6 +11,7 @@ export { Rule as ImportDestructuringSpacingRule } from './importDestructuringSpa
export { Rule as NoAttributeParameterDecoratorRule } from './noAttributeParameterDecoratorRule';
export { Rule as NoForwardRefRule } from './noForwardRefRule';
export { Rule as NoInputRenameRule } from './noInputRenameRule';
export { Rule as NoOutputNamedAfterStandardEventRule } from './noOutputNamedAfterStandardEventRule';
export { Rule as NoOutputOnPrefixRule } from './noOutputOnPrefixRule';
export { Rule as NoOutputRenameRule } from './noOutputRenameRule';
export { Rule as NoUnusedCssRule } from './noUnusedCssRule';
Expand Down
220 changes: 220 additions & 0 deletions src/noOutputNamedAfterStandardEventRule.ts
@@ -0,0 +1,220 @@
import * as Lint from 'tslint';
import * as ts from 'typescript';
import { sprintf } from 'sprintf-js';
import { NgWalker } from './angular/ngWalker';

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: 'no-output-named-after-standard-event',
type: 'maintainability',
description: `Disallows naming directive outputs after a standard DOM event.`,
rationale: `Listeners subscribed to an output with such a name will also be invoked when the native event is raised.`,
options: null,
optionsDescription: `Not configurable.`,
typescriptOnly: true,
};

static FAILURE_STRING: string = 'In the class "%s", the output ' +
'property "%s" should not be named after a standard event.';

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new OutputMetadataWalker(sourceFile,
this.getOptions()));
}
}


export class OutputMetadataWalker extends NgWalker {
// source: https://developer.mozilla.org/en-US/docs/Web/Events
private readonly standardEventNames = new Map([
['abort', true],
['afterprint', true],
['animationend', true],
['animationiteration', true],
['animationstart', true],
['appinstalled', true],
['audioprocess', true],
['audioend', true],
['audiostart', true],
['beforeprint', true],
['beforeunload', true],
['beginEvent', true],
['blocked', true],
['blur', true],
['boundary', true],
['cached', true],
['canplay', true],
['canplaythrough', true],
['change', true],
['chargingchange', true],
['chargingtimechange', true],
['checking', true],
['click', true],
['close', true],
['complete', true],
['compositionend', true],
['compositionstart', true],
['compositionupdate', true],
['contextmenu', true],
['copy', true],
['cut', true],
['dblclick', true],
['devicechange', true],
['devicelight', true],
['devicemotion', true],
['deviceorientation', true],
['deviceproximity', true],
['dischargingtimechange', true],
['DOMAttributeNameChanged', true],
['DOMAttrModified', true],
['DOMCharacterDataModified', true],
['DOMContentLoaded', true],
['DOMElementNameChanged', true],
['focus', true],
['focusin', true],
['focusout', true],
['DOMNodeInserted', true],
['DOMNodeInsertedIntoDocument', true],
['DOMNodeRemoved', true],
['DOMNodeRemovedFromDocument', true],
['DOMSubtreeModified', true],
['downloading', true],
['drag', true],
['dragend', true],
['dragenter', true],
['dragleave', true],
['dragover', true],
['dragstart', true],
['drop', true],
['durationchange', true],
['emptied', true],
['end', true],
['ended', true],
['endEvent', true],
['error', true],
['fullscreenchange', true],
['fullscreenerror', true],
['gamepadconnected', true],
['gamepaddisconnected', true],
['gotpointercapture', true],
['hashchange', true],
['lostpointercapture', true],
['input', true],
['invalid', true],
['keydown', true],
['keypress', true],
['keyup', true],
['languagechange', true],
['levelchange', true],
['load', true],
['loadeddata', true],
['loadedmetadata', true],
['loadend', true],
['loadstart', true],
['mark', true],
['message', true],
['messageerror', true],
['mousedown', true],
['mouseenter', true],
['mouseleave', true],
['mousemove', true],
['mouseout', true],
['mouseover', true],
['mouseup', true],
['nomatch', true],
['notificationclick', true],
['noupdate', true],
['obsolete', true],
['offline', true],
['online', true],
['open', true],
['orientationchange', true],
['pagehide', true],
['pageshow', true],
['paste', true],
['pause', true],
['pointercancel', true],
['pointerdown', true],
['pointerenter', true],
['pointerleave', true],
['pointerlockchange', true],
['pointerlockerror', true],
['pointermove', true],
['pointerout', true],
['pointerover', true],
['pointerup', true],
['play', true],
['playing', true],
['popstate', true],
['progress', true],
['push', true],
['pushsubscriptionchange', true],
['ratechange', true],
['readystatechange', true],
['repeatEvent', true],
['reset', true],
['resize', true],
['resourcetimingbufferfull', true],
['result', true],
['resume', true],
['scroll', true],
['seeked', true],
['seeking', true],
['select', true],
['selectstart', true],
['selectionchange', true],
['show', true],
['soundend', true],
['soundstart', true],
['speechend', true],
['speechstart', true],
['stalled', true],
['start', true],
['storage', true],
['submit', true],
['success', true],
['suspend', true],
['SVGAbort', true],
['SVGError', true],
['SVGLoad', true],
['SVGResize', true],
['SVGScroll', true],
['SVGUnload', true],
['SVGZoom', true],
['timeout', true],
['timeupdate', true],
['touchcancel', true],
['touchend', true],
['touchmove', true],
['touchstart', true],
['transitionend', true],
['unload', true],
['updateready', true],
['upgradeneeded', true],
['userproximity', true],
['voiceschanged', true],
['versionchange', true],
['visibilitychange', true],
['volumechange', true],
['waiting', true],
['wheel', true]
]);

visitNgOutput(property: ts.PropertyDeclaration, output: ts.Decorator, args: string[]) {
let className = (<any>property).parent.name.text;
let memberName = (<any>property.name).text;

if (memberName && this.standardEventNames.get(memberName)) {
const failureConfig: string[] = [Rule.FAILURE_STRING, className, memberName];
const errorMessage = sprintf.apply(null, failureConfig);
this.addFailure(
this.createFailure(
property.getStart(),
property.getWidth(),
errorMessage
)
);
}
}
}
59 changes: 59 additions & 0 deletions test/noOutputNamedAfterStandardEventRule.spec.ts
@@ -0,0 +1,59 @@
import { assertSuccess, assertAnnotated } from './testHelper';

describe('no-output-named-after-standard-event', () => {
describe('invalid directive output property', () => {
it('should fail, when component output property is named with change prefix', () => {
const source = `
@Component()
class ButtonComponent {
@Output() change = new EventEmitter<any>();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
`;

assertAnnotated({
ruleName: 'no-output-named-after-standard-event',
message: 'In the class "ButtonComponent", the output property "change" should not be named after a standard event.',
source
});
});

it('should fail, when directive output property is named with change prefix', () => {
const source = `
@Directive()
class ButtonDirective {
@Output() change = new EventEmitter<any>();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
`;

assertAnnotated({
ruleName: 'no-output-named-after-standard-event',
message: 'In the class "ButtonDirective", the output property "change" should not be named after a standard event.',
source
});
});
});

describe('valid directive output property', () => {
it('should succeed, when a component output property is properly named', () => {
const source = `
@Component()
class ButtonComponent {
@Output() buttonChange = new EventEmitter<any>();
}
`;
assertSuccess('no-output-named-after-standard-event', source);
});

it('should succeed, when a directive output property is properly named', () => {
const source = `
@Directive()
class ButtonDirective {
@Output() buttonChange = new EventEmitter<any>();
}
`;
assertSuccess('no-output-named-after-standard-event', source);
});
});
});

0 comments on commit ae3f07b

Please sign in to comment.