Skip to content

Commit 79e6caf

Browse files
committed
Allow partial transforms
1 parent ade0a34 commit 79e6caf

9 files changed

Lines changed: 118 additions & 29 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The codemod accepts the following options, passed as CLI arguments, set in a `.c
3434
| `--classic-decorator` / `classicDecorator` | `boolean` | `true` | Enable/disable adding the [`@classic` decorator](https://github.com/pzuraq/ember-classic-decorator), which helps with transitioning Ember Octane |
3535
| `--type` / `type` | `'services' \| 'routes' \| 'components' \| 'controllers' \| undefined`' | `undefined` | Apply transformation to only passed type. If `undefined, will match all types in path. |
3636
| `--quote` / `quote` | `'single' \| 'double'` | `'single'` | Whether to use double or single quotes by default for new statements that are added during the codemod. |
37+
| `--partial-transforms` / `partialTransforms` | `boolean` | `true` | If `false`, the entire file will fail validation if any EmberObject within it fails validation. |
3738
| `--ignore-leaking-state` / `ignoreLeakingState` | `string[]` | `['queryParams']` | Allow-list for `ObjectExpression` or `ArrayExpression` properties to ignore issues detailed in [eslint-plugin-ember/avoid-leaking-state-in-ember-objects](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/avoid-leaking-state-in-ember-objects.md). In the classic class syntax, using arrays and objects as default properties causes their state to "leak" between instances. If you have custom properties where you know that the shared state won't be a problem (for example, read-only configuration values), you can use this config to ignore them. NOTE: Passing this option will override the defaults, so ensure you include `'queryParams'` in the list unless you explicitly wish to disallow it. |
3839
| `DecoratorsConfig` | An object with the following properties. | See below. | Allow-list for decorators currently applied to object literal properties that can be safely applied to class properties. Pass as a comma-separated string if using as a CLI-option. Otherwise pass as an array of strings. |
3940
| `DecoratorsConfig.inObjectLiterals` | `string \| string[]` | `[]` | Allow-list for decorators currently applied to object literal properties that can be safely applied to class properties. Pass as a comma-separated string if using as a CLI-option. Otherwise pass as an array of strings. NOTE: Decorators on object methods will be allowed by default. |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import EmberObject from '@ember/object';
2+
import { inject as service } from '@ember/service';
3+
4+
// Valid, but should fail with partial transforms disabled
5+
const Foo1 = EmberObject.extend({
6+
store: service('store'),
7+
});
8+
9+
// Should fail
10+
const Foo2 = EmberObject.extend({
11+
macroValue: macro(),
12+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"partialTransforms": false
3+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import EmberObject from '@ember/object';
2+
import { inject as service } from '@ember/service';
3+
4+
// Valid, but should fail with partial transforms disabled
5+
const Foo1 = EmberObject.extend({
6+
store: service('store'),
7+
});
8+
9+
// Should fail
10+
const Foo2 = EmberObject.extend({
11+
macroValue: macro(),
12+
})
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import EmberObject from '@ember/object';
2+
import { inject as service } from '@ember/service';
3+
4+
// Should succeed
5+
const Foo1 = EmberObject.extend({
6+
store: service('store'),
7+
});
8+
9+
// Should fail
10+
const Foo2 = EmberObject.extend({
11+
macroValue: macro(),
12+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import classic from 'ember-classic-decorator';
2+
import { inject as service } from '@ember/service';
3+
import EmberObject from '@ember/object';
4+
5+
// Should succeed
6+
@classic
7+
class Foo1 extends EmberObject {
8+
@service('store')
9+
store;
10+
}
11+
12+
// Should fail
13+
const Foo2 = EmberObject.extend({
14+
macroValue: macro(),
15+
})

transforms/helpers/eo-extend-expression.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ import { createIdentifierDecorator } from './decorator-helper';
44
import type { DecoratorImportInfoMap } from './decorator-info';
55
import type { EOClassDecorator, EOProp } from './eo-prop/index';
66
import makeEOProp, { isEOClassDecorator, isEOProp } from './eo-prop/index';
7-
import logger from './log-helper';
87
import type { Options } from './options';
98
import { getClassName, getExpressionToReplace } from './parse-helper';
109
import { withComments } from './transform-helper';
1110
import type { DecoratorImportSpecs } from './util/index';
1211

12+
export interface TransformResult {
13+
className: string;
14+
success: boolean;
15+
errors: readonly string[];
16+
}
17+
1318
export default class EOExtendExpression {
1419
private className: string;
1520
private superClassName: string;
16-
21+
private properties: Array<EOClassDecorator | EOProp>;
1722
private expression: AST.EOExpression | null = null;
1823
private mixins: AST.EOMixin[];
19-
readonly properties: Array<EOClassDecorator | EOProp>;
24+
private errors: readonly string[] = [];
2025

2126
constructor(
2227
private path: AST.Path<AST.EOExtendExpression>,
23-
private filePath: string,
28+
filePath: string,
2429
existingDecoratorImportInfos: DecoratorImportInfoMap,
2530
private options: Options
2631
) {
@@ -52,17 +57,25 @@ export default class EOExtendExpression {
5257
);
5358
}
5459

55-
transform(): boolean {
60+
transform(): TransformResult {
61+
const result: TransformResult = {
62+
className: this.className,
63+
success: false,
64+
errors: [],
65+
};
66+
5667
const es6ClassDeclaration = this.build();
5768
if (es6ClassDeclaration) {
5869
const expressionToReplace = getExpressionToReplace(this.path);
5970
j(expressionToReplace).replaceWith(
6071
withComments(es6ClassDeclaration, expressionToReplace.value)
6172
);
62-
return true;
73+
result.success = true;
6374
} else {
64-
return false;
75+
result.errors = this.errors;
6576
}
77+
78+
return result;
6679
}
6780

6881
/**
@@ -106,10 +119,6 @@ export default class EOExtendExpression {
106119
private build(): AST.ClassDeclaration | null {
107120
const errors = this.validate();
108121
if (errors.length > 0) {
109-
const message = errors.join('\n\t');
110-
logger.error(
111-
`[${this.filePath}]: FAILURE \nValidation errors for class '${this.className}': \n\t${message}`
112-
);
113122
return null;
114123
}
115124

@@ -204,7 +213,7 @@ export default class EOExtendExpression {
204213
errors = [...errors, ...prop.errors];
205214
}
206215

207-
return errors;
216+
return (this.errors = errors);
208217
}
209218

210219
private makeError(message: string): string {

transforms/helpers/options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const UserOptionsSchema = z.object({
2525
classFields: z.boolean(),
2626
/** Enable/disable adding the [`@classic` decorator](https://github.com/pzuraq/ember-classic-decorator), which helps with transitioning Ember Octane */
2727
classicDecorator: z.boolean(),
28+
/** If `false`, the entire file will fail validation if any EmberObject within it fails validation. */
29+
partialTransforms: z.boolean(),
2830
/** Whether to use double or single quotes by default for new statements that are added during the codemod. */
2931
quote: z.union([z.literal('single'), z.literal('double')]),
3032
quotes: z.union([z.literal('single'), z.literal('double')]).optional(),
@@ -91,5 +93,6 @@ export const DEFAULT_OPTIONS: UserOptions = {
9193
classFields: true,
9294
classicDecorator: true,
9395
quote: 'single',
96+
partialTransforms: true,
9497
ignoreLeakingState: ['queryParams'],
9598
};

transforms/helpers/transform.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getTelemetryFor } from 'ember-codemods-telemetry-helpers';
22
import path from 'path';
33
import type * as AST from './ast';
4+
import type { TransformResult } from './eo-extend-expression';
45
import EOExtendExpression from './eo-extend-expression';
56
import {
67
createDecoratorImportDeclarations,
@@ -64,21 +65,39 @@ export default function maybeTransformEmberObjects(
6465
runtimeData,
6566
};
6667

67-
const { transformed, decoratorImportSpecs } = _maybeTransformEmberObjects(
68+
const { results, decoratorImportSpecs } = _maybeTransformEmberObjects(
6869
root,
6970
filePath,
7071
options
7172
);
7273

73-
// Need to find another way, as there might be a case where
74-
// one object from a file is transformed and other is not
75-
if (transformed) {
76-
const decoratorsToImport = Object.keys(decoratorImportSpecs).filter(
77-
(key) => decoratorImportSpecs[key as keyof DecoratorImportSpecs]
78-
);
79-
createDecoratorImportDeclarations(root, decoratorsToImport, options);
80-
logger.info(`[${filePath}]: SUCCESS`);
74+
let transformed = results.length > 0 && results.every((r) => r.success);
75+
76+
for (const result of results) {
77+
if (result.success) {
78+
if (options.partialTransforms) {
79+
transformed = true;
80+
logger.info(
81+
`[${filePath}]: SUCCESS: Transformed class '${result.className}' with no errors`
82+
);
83+
} else {
84+
logger.error(
85+
`[${filePath}]: FAILURE \nCould not transform class '${result.className}'. Need option '--partial-transforms=true'`
86+
);
87+
}
88+
} else {
89+
const message = result.errors.join('\n\t');
90+
logger.error(
91+
`[${filePath}]: FAILURE \nValidation errors for class '${result.className}': \n\t${message}`
92+
);
93+
}
8194
}
95+
96+
const decoratorsToImport = Object.keys(decoratorImportSpecs).filter(
97+
(key) => decoratorImportSpecs[key as keyof DecoratorImportSpecs]
98+
);
99+
createDecoratorImportDeclarations(root, decoratorsToImport, options);
100+
82101
return transformed;
83102
}
84103

@@ -87,12 +106,12 @@ function _maybeTransformEmberObjects(
87106
filePath: string,
88107
options: Options
89108
): {
90-
transformed: boolean;
109+
results: TransformResult[];
91110
decoratorImportSpecs: DecoratorImportSpecs;
92111
} {
93112
// Parse the import statements
94113
const existingDecoratorImportInfos = getExistingDecoratorImportInfos(root);
95-
let transformed = false;
114+
const results: TransformResult[] = [];
96115
let decoratorImportSpecs: DecoratorImportSpecs = {
97116
action: false,
98117
classNames: false,
@@ -122,13 +141,16 @@ function _maybeTransformEmberObjects(
122141
options
123142
);
124143

125-
transformed = extendExpression.transform();
144+
const result = extendExpression.transform();
145+
results.push(result);
126146

127-
decoratorImportSpecs = mergeDecoratorImportSpecs(
128-
extendExpression.decoratorImportSpecs,
129-
decoratorImportSpecs
130-
);
147+
if (result.success) {
148+
decoratorImportSpecs = mergeDecoratorImportSpecs(
149+
extendExpression.decoratorImportSpecs,
150+
decoratorImportSpecs
151+
);
152+
}
131153
});
132154

133-
return { transformed, decoratorImportSpecs };
155+
return { results, decoratorImportSpecs };
134156
}

0 commit comments

Comments
 (0)