Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Commit

Permalink
Support granular config in 'object-literal-shorthand' (#4842)
Browse files Browse the repository at this point in the history
  • Loading branch information
pablobirukov authored and adidahiya committed Sep 5, 2019
1 parent 44947c5 commit 0f2a540
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 71 deletions.
173 changes: 123 additions & 50 deletions src/rules/objectLiteralShorthandRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,19 @@ import * as ts from "typescript";

import * as Lint from "..";

const OPTION_NEVER = "never";
const OPTION_VALUE_NEVER = "never";
const OPTION_KEY_PROPERTY = "property";
const OPTION_KEY_METHOD = "method";

interface RawOptions {
[OPTION_KEY_PROPERTY]?: "never" | "always";
[OPTION_KEY_METHOD]?: "never" | "always";
}

interface Options {
enforceShorthandMethods: boolean;
enforceShorthandProperties: boolean;
}

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
Expand All @@ -37,12 +49,43 @@ export class Rule extends Lint.Rules.AbstractRule {
description: "Enforces/disallows use of ES6 object literal shorthand.",
hasFix: true,
optionsDescription: Lint.Utils.dedent`
If the \'never\' option is provided, any shorthand object literal syntax will cause a failure.`,
\`"always"\` assumed to be default option, thus with no options provided
the rule enforces object literal methods and properties shorthands.
With \`"never"\` option provided, any shorthand object literal syntax causes an error.
The rule can be configured in a more granular way.
With \`{"property": "never"}\` provided (which is equivalent to \`{"property": "never", "method": "always"}\`),
the rule only flags property shorthand assignments,
and respectively with \`{"method": "never"}\` (equivalent to \`{"property": "always", "method": "never"}\`),
the rule fails only on method shorthands.`,
options: {
type: "string",
enum: [OPTION_NEVER],
oneOf: [
{
type: "string",
enum: [OPTION_VALUE_NEVER],
},
{
type: "object",
properties: {
[OPTION_KEY_PROPERTY]: {
type: "string",
enum: [OPTION_VALUE_NEVER],
},
[OPTION_KEY_METHOD]: {
type: "string",
enum: [OPTION_VALUE_NEVER],
},
},
minProperties: 1,
maxProperties: 2,
},
],
},
optionExamples: [true, [true, OPTION_NEVER]],
optionExamples: [
true,
[true, OPTION_VALUE_NEVER],
[true, { [OPTION_KEY_PROPERTY]: OPTION_VALUE_NEVER }],
],
type: "style",
typescriptOnly: false,
};
Expand All @@ -52,74 +95,104 @@ export class Rule extends Lint.Rules.AbstractRule {
public static LONGHAND_METHOD = "Expected method shorthand in object literal ";
public static SHORTHAND_ASSIGNMENT = "Shorthand property assignments have been disallowed.";

public static getLonghandPropertyErrorMessage(nodeText: string) {
return `Expected property shorthand in object literal ('${nodeText}').`;
}
public static getLonghandMethodErrorMessage(nodeText: string) {
return `Expected method shorthand in object literal ('${nodeText}').`;
}
public static getDisallowedShorthandErrorMessage(options: Options) {
if (options.enforceShorthandMethods && !options.enforceShorthandProperties) {
return "Shorthand property assignments have been disallowed.";
} else if (!options.enforceShorthandMethods && options.enforceShorthandProperties) {
return "Shorthand method assignments have been disallowed.";
}
return "Shorthand property and method assignments have been disallowed.";
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(
sourceFile,
this.ruleArguments.indexOf(OPTION_NEVER) === -1
? enforceShorthandWalker
: disallowShorthandWalker,
return this.applyWithFunction(sourceFile, walk, this.parseOptions(this.ruleArguments));
}

private parseOptions(options: Array<string | RawOptions>): Options {
if (options.indexOf(OPTION_VALUE_NEVER) !== -1) {
return {
enforceShorthandMethods: false,
enforceShorthandProperties: false,
};
}
const optionsObject: RawOptions | undefined = options.find(
(el: string | RawOptions): el is RawOptions =>
typeof el === "object" &&
(el[OPTION_KEY_PROPERTY] === OPTION_VALUE_NEVER ||
el[OPTION_KEY_METHOD] === OPTION_VALUE_NEVER),
);
if (optionsObject !== undefined) {
return {
enforceShorthandMethods: !(optionsObject[OPTION_KEY_METHOD] === OPTION_VALUE_NEVER),
enforceShorthandProperties: !(
optionsObject[OPTION_KEY_PROPERTY] === OPTION_VALUE_NEVER
),
};
} else {
return {
enforceShorthandMethods: true,
enforceShorthandProperties: true,
};
}
}
}

function disallowShorthandWalker(ctx: Lint.WalkContext) {
function walk(ctx: Lint.WalkContext<Options>) {
const { enforceShorthandMethods, enforceShorthandProperties } = ctx.options;
return ts.forEachChild(ctx.sourceFile, function cb(node): void {
if (isShorthandPropertyAssignment(node)) {
if (
enforceShorthandProperties &&
isPropertyAssignment(node) &&
node.name.kind === ts.SyntaxKind.Identifier &&
isIdentifier(node.initializer) &&
node.name.text === node.initializer.text
) {
ctx.addFailureAtNode(
node,
Rule.getLonghandPropertyErrorMessage(`{${node.name.text}}`),
Lint.Replacement.deleteFromTo(node.name.end, node.end),
);
} else if (
enforceShorthandMethods &&
isPropertyAssignment(node) &&
isFunctionExpression(node.initializer) &&
// allow named function expressions
node.initializer.name === undefined
) {
const [name, fix] = handleLonghandMethod(node.name, node.initializer, ctx.sourceFile);
ctx.addFailure(
node.getStart(ctx.sourceFile),
getChildOfKind(node.initializer, ts.SyntaxKind.OpenParenToken, ctx.sourceFile)!.pos,
Rule.getLonghandMethodErrorMessage(`{${name}() {...}}`),
fix,
);
} else if (!enforceShorthandProperties && isShorthandPropertyAssignment(node)) {
ctx.addFailureAtNode(
node.name,
Rule.SHORTHAND_ASSIGNMENT,
Rule.getDisallowedShorthandErrorMessage(ctx.options),
Lint.Replacement.appendText(node.getStart(ctx.sourceFile), `${node.name.text}: `),
);
} else if (
!enforceShorthandMethods &&
isMethodDeclaration(node) &&
node.parent.kind === ts.SyntaxKind.ObjectLiteralExpression
) {
ctx.addFailureAtNode(
node.name,
Rule.SHORTHAND_ASSIGNMENT,
Rule.getDisallowedShorthandErrorMessage(ctx.options),
fixShorthandMethodDeclaration(node, ctx.sourceFile),
);
}
return ts.forEachChild(node, cb);
});
}

function enforceShorthandWalker(ctx: Lint.WalkContext) {
return ts.forEachChild(ctx.sourceFile, function cb(node): void {
if (isPropertyAssignment(node)) {
if (
node.name.kind === ts.SyntaxKind.Identifier &&
isIdentifier(node.initializer) &&
node.name.text === node.initializer.text
) {
ctx.addFailureAtNode(
node,
`${Rule.LONGHAND_PROPERTY}('{${node.name.text}}').`,
Lint.Replacement.deleteFromTo(node.name.end, node.end),
);
} else if (
isFunctionExpression(node.initializer) &&
// allow named function expressions
node.initializer.name === undefined
) {
const [name, fix] = handleLonghandMethod(
node.name,
node.initializer,
ctx.sourceFile,
);
ctx.addFailure(
node.getStart(ctx.sourceFile),
getChildOfKind(node.initializer, ts.SyntaxKind.OpenParenToken, ctx.sourceFile)!
.pos,
`${Rule.LONGHAND_METHOD}('{${name}() {...}}').`,
fix,
);
}
}
return ts.forEachChild(node, cb);
});
}

function fixShorthandMethodDeclaration(node: ts.MethodDeclaration, sourceFile: ts.SourceFile) {
const isGenerator = node.asteriskToken !== undefined;
const isAsync = hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
Expand Down
18 changes: 10 additions & 8 deletions test/rules/object-literal-shorthand/always/test.ts.lint
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const bad = {
w: function() {},
~~~~~~~~~~~ [Expected method shorthand in object literal ('{w() {...}}').]
~~~~~~~~~~~ [LONGHAND_METHOD % ("('{w() {...}}')")]
x: function *() {},
~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{*x() {...}}').]
~~~~~~~~~~~~~ [LONGHAND_METHOD % ("('{*x() {...}}')")]
[y]: function() {},
~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{[y]() {...}}').]
~~~~~~~~~~~~~ [LONGHAND_METHOD % ("('{[y]() {...}}')")]
z: z
~~~~ [Expected property shorthand in object literal ('{z}').]
};
Expand All @@ -26,7 +26,7 @@ const namedFunctions = {

const quotes = {
"foo-bar": function() {},
~~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{"foo-bar"() {...}}').]
~~~~~~~~~~~~~~~~~~~ [LONGHAND_METHOD % ("('{\"foo-bar\"() {...}}')")]
"foo-bar"() {}
};

Expand All @@ -43,11 +43,13 @@ const extraCases = {

const asyncFn = {
foo: async function() {},
~~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{async foo() {...}}').]
~~~~~~~~~~~~~~~~~~~ [LONGHAND_METHOD % ("('{async foo() {...}}')")]
bar: async function*() {}
~~~~~~~~~~~~~~~~~~~~ [Expected method shorthand in object literal ('{async *bar() {...}}').]
~~~~~~~~~~~~~~~~~~~~ [LONGHAND_METHOD % ("('{async *bar() {...}}')")]
}

({foo: foo} = {foo: foo});
~~~~~~~~ [Expected property shorthand in object literal ('{foo}').]
~~~~~~~~ [Expected property shorthand in object literal ('{foo}').]
~~~~~~~~ [LONGHAND_PROPERTY % ("('{foo}')")]
~~~~~~~~ [LONGHAND_PROPERTY % ("('{foo}')")]
[LONGHAND_METHOD]: Expected method shorthand in object literal %s.
[LONGHAND_PROPERTY]: Expected property shorthand in object literal %s.
26 changes: 13 additions & 13 deletions test/rules/object-literal-shorthand/never/test.ts.lint
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
const asyncFn = {
async f() {
~ [OBJECT_LITERAL_DISALLOWED]
~ [SHORTHAND_ASSIGNMENT]
await some_promise;
},
async* fa() {
~~ [OBJECT_LITERAL_DISALLOWED]
~~ [SHORTHAND_ASSIGNMENT]
await some_promise;
}
};

const bad = {
w() {
~ [OBJECT_LITERAL_DISALLOWED]
~ [SHORTHAND_ASSIGNMENT]
const alsoBad = {
bad,
~~~ [OBJECT_LITERAL_DISALLOWED]
~~~ [SHORTHAND_ASSIGNMENT]
};
},
*x() {},
~ [OBJECT_LITERAL_DISALLOWED]
~ [SHORTHAND_ASSIGNMENT]
[y]() {},
~~~ [OBJECT_LITERAL_DISALLOWED]
~~~ [SHORTHAND_ASSIGNMENT]
z,
~ [OBJECT_LITERAL_DISALLOWED]
~ [SHORTHAND_ASSIGNMENT]
nest: {
nestBad() {},
~~~~~~~ [OBJECT_LITERAL_DISALLOWED]
~~~~~~~ [SHORTHAND_ASSIGNMENT]
nextGood: function(prop: string): void {}
}
};
Expand All @@ -47,12 +47,12 @@ const namedFunctions = {
const quotes = {
"foo-bar": function() {},
"foo-bar"() {}
~~~~~~~~~ [OBJECT_LITERAL_DISALLOWED]
~~~~~~~~~ [SHORTHAND_ASSIGNMENT]
};

const extraCases = {
x,
~ [OBJECT_LITERAL_DISALLOWED]
~ [SHORTHAND_ASSIGNMENT]
a: 123,
b: "hello",
c: 'c',
Expand All @@ -66,7 +66,7 @@ export class ClassA extends ClassZ {
}

({foo} = {foo});
~~~ [OBJECT_LITERAL_DISALLOWED]
~~~ [OBJECT_LITERAL_DISALLOWED]
~~~ [SHORTHAND_ASSIGNMENT]
~~~ [SHORTHAND_ASSIGNMENT]

[OBJECT_LITERAL_DISALLOWED]: Shorthand property assignments have been disallowed.
[SHORTHAND_ASSIGNMENT]: Shorthand property and method assignments have been disallowed.
45 changes: 45 additions & 0 deletions test/rules/object-literal-shorthand/onlyMethods/test.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const badMethodsGoodProps = {
w() {},
*x() {},
[y]() {},
z: z
};

const goodMethodsBadProps = {
w() {},
*x() {},
[y]() {},
z: z
};

const arrows = {
x: (y) => y // this is OK.
};

const namedFunctions = {
x: function y() {} // named function expressions are also OK.
};

const quotes = {
"foo-bar"() {},
"foo-bar"() {}
};

const extraCases = {
x: x,
a: 123,
b: "hello",
c: 'c',
["a" + "nested"]: {
x: x
}
};

const asyncFn = {
async foo() {},
async *bar() {}
}

({foo: foo} = {foo: foo});
({foo: foo} = {foo: foo});

Loading

0 comments on commit 0f2a540

Please sign in to comment.