diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json
index f120e81283a76..dcf0530241509 100644
--- a/src/compiler/diagnosticMessages.json
+++ b/src/compiler/diagnosticMessages.json
@@ -3965,5 +3965,9 @@
"Convert to ES6 module": {
"category": "Message",
"code": 95017
+ },
+ "Generate 'get' and 'set' accessors": {
+ "category": "Message",
+ "code": 95018
}
}
diff --git a/src/services/refactors/ConvertToGetterAndSetter.ts b/src/services/refactors/ConvertToGetterAndSetter.ts
new file mode 100644
index 0000000000000..0c5cc6710931f
--- /dev/null
+++ b/src/services/refactors/ConvertToGetterAndSetter.ts
@@ -0,0 +1,151 @@
+/* @internal */
+namespace ts.refactor.ConvertGetterAndSetter {
+ const actionName = "Generate 'get' and 'set' accessors";
+ const actionDescription = Diagnostics.Generate_get_and_set_accessors.message;
+
+ registerRefactor(actionName, { getEditsForAction, getAvailableActions });
+
+ function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
+ const { file, startPosition } = context;
+
+ const fieldInfo = getConvertibleFieldAtPosition(file, startPosition);
+ if (!fieldInfo) return undefined;
+
+ return [
+ {
+ name: actionName,
+ description: actionDescription,
+ actions: [
+ {
+ name: actionName,
+ description: actionDescription
+ }
+ ]
+ }
+ ];
+ }
+
+ function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined {
+ const { file, startPosition } = context;
+
+ const fieldInfo = getConvertibleFieldAtPosition(file, startPosition);
+ if (!fieldInfo) return undefined;
+
+ const changeTracker = textChanges.ChangeTracker.fromContext(context);
+ const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
+
+ const { fieldName, accessorName, propertyDeclaration, needUpdateName, hasModifiers, needUpdateModifiers } = fieldInfo;
+ const accessorModifiers = hasModifiers ? createNodeArray([createToken(SyntaxKind.PublicKeyword)]) : undefined;
+
+ const getAccessor = generateGetAccessor(propertyDeclaration, fieldName, accessorName, accessorModifiers);
+ const setAccessor = generateSetAccessor(propertyDeclaration, fieldName, accessorName, accessorModifiers);
+
+ const modifiers = needUpdateModifiers ? createNodeArray([createToken(SyntaxKind.PrivateKeyword)]) : propertyDeclaration.modifiers;
+ if (needUpdateName || needUpdateModifiers) {
+ changeTracker.replaceNode(file, propertyDeclaration, updateOriginPropertyDeclaration(propertyDeclaration, fieldName, modifiers), {
+ suffix: newLineCharacter
+ });
+ }
+
+ changeTracker.insertNodeAfter(file, propertyDeclaration, getAccessor);
+ changeTracker.insertNodeAfter(file, propertyDeclaration, setAccessor);
+
+ return {
+ edits: changeTracker.getChanges(),
+ renameFilename: undefined,
+ renameLocation: undefined,
+ };
+ }
+
+ interface Info { originName: string; fieldName: string; accessorName: string; propertyDeclaration: PropertyDeclaration; needUpdateName: boolean; hasModifiers: boolean; needUpdateModifiers: boolean; }
+ 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.kind === SyntaxKind.Identifier &&
+ (getModifierFlags(propertyDeclaration) | ModifierFlags.AccessibilityModifier) === ModifierFlags.AccessibilityModifier)) return undefined;
+
+ const containerClass = getContainingClass(propertyDeclaration);
+ if (!containerClass) return undefined;
+
+ const members = getMembersOfDeclaration(containerClass);
+ if (!members) return undefined;
+
+ const needUpdateName = propertyDeclaration.name.text.charCodeAt(0) !== CharacterCodes._;
+
+ 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;
+
+ const hasModifiers = !!find(members, member => !!member.modifiers);
+ const needUpdateModifiers = hasModifiers && (!propertyDeclaration.modifiers || hasModifier(propertyDeclaration, ModifierFlags.Public));
+
+ return {
+ originName: propertyDeclaration.name.text,
+ fieldName,
+ accessorName,
+ propertyDeclaration,
+ needUpdateName,
+ hasModifiers,
+ needUpdateModifiers
+ };
+ }
+
+ function generateGetAccessor (propertyDeclaration: PropertyDeclaration, fieldName: string, name: string, modifiers: ModifiersArray) {
+ return createGetAccessor(
+ /*decorators*/ undefined,
+ modifiers,
+ name,
+ /*parameters*/ undefined,
+ propertyDeclaration.type,
+ createBlock([
+ createReturn(
+ createPropertyAccess(
+ createThis(),
+ fieldName
+ )
+ )
+ ], /*multiLine*/ true)
+ );
+ }
+
+ function generateSetAccessor (propertyDeclaration: PropertyDeclaration, fieldName: string, name: string, modifiers: ModifiersArray) {
+ return createSetAccessor(
+ /*decorators*/ undefined,
+ modifiers,
+ name,
+ [createParameter(
+ /*decorators*/ undefined,
+ /*modifies*/ undefined,
+ /*dotDotDotToken*/ undefined,
+ createIdentifier("value"),
+ /*questionToken*/ undefined,
+ propertyDeclaration.type
+ )],
+ createBlock([
+ createStatement(
+ createAssignment(
+ createPropertyAccess(
+ createThis(),
+ fieldName
+ ),
+ createIdentifier("value")
+ )
+ )
+ ], /*multiLine*/ true)
+ );
+ }
+
+ function updateOriginPropertyDeclaration (propertyDeclaration: PropertyDeclaration, fieldName: string, modifiers: ModifiersArray) {
+ return updateProperty(
+ propertyDeclaration,
+ /*decorators*/ undefined,
+ modifiers,
+ fieldName,
+ /*questionOrExclamationToken*/ undefined,
+ propertyDeclaration.type,
+ propertyDeclaration.initializer,
+ );
+ }
+}
diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts
index 8b4561700d554..48a3870b9a1ea 100644
--- a/src/services/refactors/refactors.ts
+++ b/src/services/refactors/refactors.ts
@@ -4,3 +4,4 @@
///
///
///
+///
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess1.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess1.ts
new file mode 100644
index 0000000000000..90b6dce8aa10a
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess1.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/public a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ private _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess10.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess10.ts
new file mode 100644
index 0000000000000..8fa18e861b480
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess10.ts
@@ -0,0 +1,23 @@
+///
+
+//// class A {
+//// public a: string;
+//// /*a*/b: number;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ public a: string;
+ private _b: number;
+ public get b(): number {
+ return this._b;
+ }
+ public set b(value: number) {
+ this._b = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess11.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess11.ts
new file mode 100644
index 0000000000000..91e209a5d263f
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess11.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/public a: string = "foo";/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ private _a: string = "foo";
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess2.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess2.ts
new file mode 100644
index 0000000000000..aa8c77313dd97
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess2.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/protected a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ protected _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess3.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess3.ts
new file mode 100644
index 0000000000000..4f26ed33366cf
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess3.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/private a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ private _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess4.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess4.ts
new file mode 100644
index 0000000000000..1597244e29f4a
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess4.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/private _a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ private _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess5.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess5.ts
new file mode 100644
index 0000000000000..16edc3515d379
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess5.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/protected _a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ protected _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess6.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess6.ts
new file mode 100644
index 0000000000000..476a1f851e05b
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess6.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/public _a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ private _a: string;
+ public get a(): string {
+ return this._a;
+ }
+ public set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess7.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess7.ts
new file mode 100644
index 0000000000000..e2f2022db8c63
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess7.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/_a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ _a: string;
+ get a(): string {
+ return this._a;
+ }
+ set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess8.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess8.ts
new file mode 100644
index 0000000000000..c6bfda108664c
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess8.ts
@@ -0,0 +1,21 @@
+///
+
+//// class A {
+//// /*a*/a: string;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ _a: string;
+ get a(): string {
+ return this._a;
+ }
+ set a(value: string) {
+ this._a = value;
+ }
+}`,
+});
diff --git a/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess9.ts b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess9.ts
new file mode 100644
index 0000000000000..be4c47967a570
--- /dev/null
+++ b/tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess9.ts
@@ -0,0 +1,23 @@
+///
+
+//// class A {
+//// a: string;
+//// /*a*/b: number;/*b*/
+//// }
+
+goTo.select("a", "b");
+edit.applyRefactor({
+ refactorName: "Generate 'get' and 'set' accessors",
+ actionName: "Generate 'get' and 'set' accessors",
+ actionDescription: "Generate 'get' and 'set' accessors",
+ newContent: `class A {
+ a: string;
+ _b: number;
+ get b(): number {
+ return this._b;
+ }
+ set b(value: number) {
+ this._b = value;
+ }
+}`,
+});