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

Support granular config in 'object-literal-shorthand' #4842

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
173 changes: 123 additions & 50 deletions src/rules/objectLiteralShorthandRule.ts
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
@@ -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
@@ -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
@@ -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});