Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add refactor of convert private field to getter and setter #22143

Merged
9 commits merged into from
Apr 10, 2018

Conversation

Kingwl
Copy link
Contributor

@Kingwl Kingwl commented Feb 23, 2018

Fixes #12417

@@ -3965,5 +3965,9 @@
"Convert to ES6 module": {
"category": "Message",
"code": 95017
},
"Convert to getter and setter": {
Copy link
Member

@DanielRosenwasser DanielRosenwasser Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Generate 'get' and 'set' accessors."

You can potentially keep it as "getter" and "setter", but I don't know how well that localizes. I think the key thing I have in mind is that you should have the text "generate" in there somehow.

function createGetter (fieldName: string, name: string, type: TypeNode) {
return createGetAccessor(
/*decorators*/ undefined,
[createToken(SyntaxKind.PublicKeyword)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice if we could detect whether other public members use modifiers instead of injecting one.

function getConvertibleFieldAtPosition(file: SourceFile, startPosition: number): Info | undefined {
const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false);
const propertyDeclaration = findAncestor(node.parent, isPropertyDeclaration);
if (!(propertyDeclaration && propertyDeclaration.name.getText().charAt(0) === "_" && hasModifier(propertyDeclaration, ModifierFlags.Private))) return undefined;
Copy link
Member

@DanielRosenwasser DanielRosenwasser Feb 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give a short comment explaining why you want to do this check.

Also, use charCodeAt(0) === CharacterCodes._.

Also, break this into at least two lines and use braces please.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does this work for computed properties? You probably want to make sure that your propertyDeclaration.name is an identifier. That way you could also use propertyDeclaration.name.text after you use a type-guard.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that you have to write private _foo: number; to get this refactor. Wouldn't it make more sense to trigger on public foo: number; and convert it to private _foo: number; public get foo() {} public set foo() {}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

function getConvertibleFieldAtPosition(file: SourceFile, startPosition: number): Info | undefined {
const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false);
const propertyDeclaration = findAncestor(node.parent, isPropertyDeclaration);
if (!(propertyDeclaration && propertyDeclaration.name.getText().charAt(0) === "_" && hasModifier(propertyDeclaration, ModifierFlags.Private))) return undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does this work for computed properties? You probably want to make sure that your propertyDeclaration.name is an identifier. That way you could also use propertyDeclaration.name.text after you use a type-guard.


return {
fieldName: propertyDeclaration.name.getText(),
name: propertyDeclaration.name.getText().substring(1),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will actually need to make sure that you create a unique name that doesn't exist in the current class. For simplicity, I would just bail out if the property with this name already exists.

if (!(propertyDeclaration && propertyDeclaration.name.getText().charAt(0) === "_" && hasModifier(propertyDeclaration, ModifierFlags.Private))) return undefined;

return {
fieldName: propertyDeclaration.name.getText(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal with propertyDeclaration.name.text here and below.

actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
protected _a: string;
public get a(): string {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the initial property access is protected then this should be protected. The inner variable should be private

@@ -4,3 +4,4 @@
/// <reference path="extractSymbol.ts" />
/// <reference path="installTypesForPackage.ts" />
/// <reference path="useDefaultImport.ts" />
/// <reference path="ConvertToGetterAndSetter.ts" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: please keep file casing consistent by renaming the file and this reference to it. (first letter should be lower-case.

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting close! Make sure you also have tests for a

  • static member (maybe don't provide any quickfix here)
  • readonly member (maybe only generate a get-accessor, not clear if you want to keep the readonly modifier around)

);
}

function updateOriginPropertyDeclaration (propertyDeclaration: PropertyDeclaration, fieldName: string, modifiers: ModifiersArray) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateoriginalPropertyDeclaration

function updateOriginPropertyDeclaration (propertyDeclaration: PropertyDeclaration, fieldName: string, modifiers: ModifiersArray) {
return updateProperty(
propertyDeclaration,
/*decorators*/ undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems problematic to drop decorators here.

/*decorators*/ undefined,
modifiers,
fieldName,
/*questionOrExclamationToken*/ undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also strange to drop the token.

};
}

interface Info { originName: string; fieldName: string; accessorName: string; propertyDeclaration: PropertyDeclaration; needUpdateName: boolean; hasModifiers: boolean; needUpdateModifiers: boolean; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originName to originalName

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this on multiple lines.

//// /*a*/public a: string;/*b*/
//// }

goTo.select("a", "b");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we have a range API instead, but I haven't ever written one of these tests yet, so this might be fine?

Copy link
Contributor Author

@Kingwl Kingwl Feb 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure...
I copied it from the refactor Convert Es6 Module


if (find(members, member => needUpdateName ? member.name.getText() === fieldName : member.name.getText() === accessorName)) return undefined;

const hasModifiers = !!find(members, member => !!member.modifiers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I initially meant whether a field had a public modifier explicitly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the field is Protected

@Kingwl
Copy link
Contributor Author

Kingwl commented Feb 26, 2018

After thinking, I tend to not support readonly, because of the following two case:

1. Initializer is not a Literal (may be side effects)

public readonly a : TT = new TT;

it cannot be convert as Literal

public get a () {
  return "Literal";
}

2.

before:

public readonly a : TT = new TT;

after:

private _a: TT = new TT;

public get a () {
  return this._a
}

it can mod _a in the context of same class
this does not match the readonly semantics

or:

private readonly _a: TT = new TT;

public get a () {
  return this._a
}

it seems do nothing

@Kingwl Kingwl force-pushed the add-getter-and-setter branch 2 times, most recently from ba0f71b to e39a889 Compare February 26, 2018 08:29
//// }

goTo.select("a", "b");
verify.not.refactorAvailable("Generate 'get' and 'set' accessors");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not see why we would not allow this. you just need to come up with a temporary name that starts with _a, set the rename location to it, and let the user decide what they want to call it. you can just call createUniqueName to create you an identifier with a unique name and use it instead.

See https://github.com/Microsoft/TypeScript/blob/1ed30c6d4d78aeb7de0c81604dbf38120d82e82b/src/services/refactors/extractSymbol.ts#L1137-L1161

actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
_a: string;
get a(): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? i would expect this to be get _a

if (!fieldInfo) return undefined;

const changeTracker = textChanges.ChangeTracker.fromContext(context);
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newLineCharacter should now be available on ChangeTracker.newLineCharacter , no need to recompute it,

const propertyDeclaration = findAncestor(node.parent, isPropertyDeclaration);

if (!propertyDeclaration || propertyDeclaration.name.kind !== SyntaxKind.Identifier) return undefined;
// make sure propertyDeclaration have only AccessibilityModifier
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about static? you can have static accessors


function getConvertibleFieldAtPosition(file: SourceFile, startPosition: number): Info | undefined {
const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false);
const propertyDeclaration = findAncestor(node.parent, isPropertyDeclaration);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need to handle parameter properties. so this can be a property or a parameter declaration.

// make sure propertyDeclaration have only AccessibilityModifier
if ((getModifierFlags(propertyDeclaration) | ModifierFlags.AccessibilityModifier) !== ModifierFlags.AccessibilityModifier) return undefined;

const containerClass = getContainingClass(propertyDeclaration);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of these two lines, you can just check that isClassLike(propertyDeclaration.parent) or isClassLike(parameterDeclaration.parent.parent)

const members = getMembersOfDeclaration(containerClass);
if (!members) return undefined;

const needUpdateName = propertyDeclaration.name.text.charCodeAt(0) !== CharacterCodes._;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not think you need that. as i noted in my other comment, we should pick a unique name that has a seed of "_" + <property name>, then set the rename location on this name. the user will get the chance to rename it right there and then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so in short the name should always be updated.

const accessorName = needUpdateName ? propertyDeclaration.name.text : propertyDeclaration.name.text.substring(1);
const fieldName = `_${accessorName}`;

if (find(members, member => needUpdateName ? member.name.getText() === fieldName : member.name.getText() === accessorName)) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not think you need these either.


const hasModifiers = !!find(members, member => !!member.modifiers);
const needUpdateModifiers = hasModifiers && (!propertyDeclaration.modifiers || !hasModifier(propertyDeclaration, ModifierFlags.Private));
const accessorType = propertyDeclaration.questionToken ? mergeTypeNodeToUnion(propertyDeclaration.type, createKeywordTypeNode(SyntaxKind.UndefinedKeyword)) : propertyDeclaration.type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would say you do not need to create this type until you need to execute the refactoring. this function is called in both getAvailableActions and getEditsForAction, so ideally we would defer that until we are computing the edits.

const fieldInfo = getConvertibleFieldAtPosition(file, startPosition);
if (!fieldInfo) return undefined;

return [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, keep the [ and { on the same line.

const hasModifiers = !!find(members, member => !!member.modifiers);
const needUpdateModifiers = hasModifiers && (!propertyDeclaration.modifiers || !hasModifier(propertyDeclaration, ModifierFlags.Private));
const accessorType = propertyDeclaration.questionToken ? mergeTypeNodeToUnion(propertyDeclaration.type, createKeywordTypeNode(SyntaxKind.UndefinedKeyword)) : propertyDeclaration.type;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also keep in mind that propertyDeclaration.type may not be there. if this is the case, you want to not include a type, nor do you want to merge it with undefined.

also you should add a test for that.

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, a few comments. also please add a test for applying the refactoring on a .js file

function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
const { file, startPosition } = context;

const fieldInfo = getConvertibleFieldAtPosition(file, startPosition);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable can be inlined

const accessorName = needUpdateName ? propertyDeclaration.name.text : propertyDeclaration.name.text.substring(1);
const fieldName = `_${accessorName}`;

if (find(members, member => needUpdateName ? member.name.getText() === fieldName : member.name.getText() === accessorName)) return undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer members.some(...) over find when just using as a boolean

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, .getText() on every member might be slow since getText() has to do scanning. Might want to do something like function getDeclarationName from services.ts to just get Identifier and StringLiteral-named properties.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, getMemberName(member.name) === (needUpdateName ? fieldName : accessorName)

Copy link

@ghost ghost Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, can avoid looping over members and instead check for checker.getPropertyOfType(checker.getTypeAtLocation(containerClass.name), needUpdateName ? fieldName : accessorName) which avoids conflicts with supertypes too.

const containerClass = getContainingClass(propertyDeclaration);
if (!containerClass) return undefined;

const members = getMembersOfDeclaration(containerClass);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just containerClass.members?

const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);

const { fieldName, accessorName, accessorType, propertyDeclaration, needUpdateName, hasModifiers, needUpdateModifiers } = fieldInfo;
const accessorModifiers = hasModifiers ? (
Copy link

@ghost ghost Mar 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasModifiers currently is true if any member on the class has modifiers. Instead this could just preserve whatever modifier was there before on the particular member without looking at other members. I.e., public x: number becomes public get x() { ... } public set x() { ... } and x: number becomes get x() { ... } set x() { ... }, making hasModifiers unnecessary.

const changeTracker = textChanges.ChangeTracker.fromContext(context);
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);

const { fieldName, accessorName, accessorType, propertyDeclaration, needUpdateName, hasModifiers, needUpdateModifiers } = fieldInfo;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needUpdateName and needUpdateModifiers aren't used separately, so I would combine them to one property.

const fieldInfo = getConvertibleFieldAtPosition(file, startPosition);
if (!fieldInfo) return undefined;

const changeTracker = textChanges.ChangeTracker.fromContext(context);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: would move this closer to its use.

const modifiers = hasModifiers ? createNodeArray([createToken(SyntaxKind.PrivateKeyword)]) : undefined;
if (needUpdateName || needUpdateModifiers) {
changeTracker.replaceNode(file, propertyDeclaration, updateoriginalPropertyDeclaration(propertyDeclaration, fieldName, modifiers), {
suffix: newLineCharacter
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the latest changes to ChangeTracker it shouldn't be necessary to explicitly specify options. If it is, file an issue and we can fix it later after this PR is merged.

name,
[createParameter(
/*decorators*/ undefined,
/*modifies*/ undefined,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*modifiers

const accessorType = propertyDeclaration.questionToken ? mergeTypeNodeToUnion(propertyDeclaration.type, createKeywordTypeNode(SyntaxKind.UndefinedKeyword)) : propertyDeclaration.type;

return {
originalName: propertyDeclaration.name.text,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to make const originalName early since it's used in 3 other places.

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 3, 2018

@mhegazy
PropertyDeclaration is not existed in ClassLikeContainer in JS(probably) 😂

@mhegazy
Copy link
Contributor

mhegazy commented Apr 3, 2018

@mhegazy
PropertyDeclaration is not existed in ClassLikeContainer in JS(probably) 😂

we do allow all "standard-track" features in JS:

image

@mhegazy
Copy link
Contributor

mhegazy commented Apr 3, 2018

@Kingwl i think we should support readonly as well.. no reason not to..

class C { 
    readonly x = 0;
}

should convert to

class C { 
    private _x = 0;
    get x() { 
        return this._x;
    }
}

I think we should do that in a separate PR though, since there is some complications with constructor access here.. you want to replace all references to the readonly property in the constructor with the new one that was created and not the getter/setter. i.e.

class C { 
    readonly x: number;
    constructor() { 
        this.x = 0;
    }
}

should become:

class C { 
    private _x: number;
    get x() { 
        return this._x;
    }
    constructor () { 
        this._x = 0; // _x and not x here
    }
}

@mhegazy
Copy link
Contributor

mhegazy commented Apr 3, 2018

gave it a quick test, found two issues..

  1. JavaScript files, we should not add accessor modifiers if this is a JS file. should be a simple check in getFieldModifiers and in getAccessorModifiers. Also please add a test for this
  2. comments. we are not removing the comments from the nodes correctly before using them, you probably want to use suppressLeadingAndTrailingTrivia on the name of the field before using it, e.g.
/** class comment */
class C {
    // Field commment
    x = 22;
}

as i said before, we should look into the readonly case, but i would do that in a separate PR. if you can get these two issues fixed, we should be good to go on this PR.

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 3, 2018

Sure, but it might be late. I'm enjoying my holiday in Disney(three days)😁

@mhegazy
Copy link
Contributor

mhegazy commented Apr 3, 2018

no worries. take your time. and have a great vacation °o°

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 6, 2018

ahhhh, could refactor apply withPropertyAssign ?

@mhegazy
Copy link
Contributor

mhegazy commented Apr 6, 2018

ahhhh, could refactor apply withPropertyAssign ?

i do not see why not. good point.

@@ -462,6 +468,9 @@ namespace ts.textChanges {
else if (isVariableDeclaration(node)) {
return { prefix: ", " };
}
else if (isPropertyAssignment(node)) {
return { suffix: "," + this.newLineCharacter }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhegazy some thing need help
in this case
how should i add the , after every props
if i update the ObjectLiteralExpression, the original PropertyAssignment will overship with this change

function getPropertyAssignmentDeclarationInfo(propertyAssignment: PropertyAssignment): DeclarationInfo | undefined {
return {
isStatic: false,
type: undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what should i do with this type declaration,
should i get the type with typeChecker?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, you are fine. the type will be inferred from the return type of the getter.

@@ -72,16 +89,14 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
return declaration.modifiers;
}

function getFieldModifiers(isStatic: boolean): NodeArray<Modifier> {
function getFieldModifiers(isJS: boolean, isStatic: boolean, isClassLike: boolean): NodeArray<Modifier> | undefined {
if (isJS || !isClassLike) return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you still want to handel the static modifier in the case of js. i would jsut make it

return createNodeArray(
             append<Modifier>(isJS ? [] : [createToken(SyntaxKind.PrivateKeyword)],
                 isStatic ? createToken(SyntaxKind.StaticKeyword) : undefined));

@mhegazy
Copy link
Contributor

mhegazy commented Apr 10, 2018

@andy-ms any more comments?

@mhegazy
Copy link
Contributor

mhegazy commented Apr 10, 2018

thanks @Kingwl!. wanna get the readonly support in next?

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 11, 2018

sure 😅

@Kingwl
Copy link
Contributor Author

Kingwl commented Apr 11, 2018

wait for pr been merged which @andy-ms has send and thanks for the review 😬

@Kingwl
Copy link
Contributor Author

Kingwl commented May 8, 2018

thanks all for @mhegazy @andy-ms 's review and improve
i'd like to add readonly support right now

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2018

Go for it.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants