Push-down field
Introduction
Although it was planned to use a field universally for all classes, in reality the field is used only in some subclasses. This situation can occur when planned features fail to pan out, for example. because of this, we push down the field from the superclass into its related subclass.
Pre and Post Conditions
Pre Conditions:
-
There should exist a corresponding child and parent in the project.
-
The field that should be pushed down must be valid.
-
The user must enter the package's name, class's name and the fields that need to be added.
Post Conditions:
-
The changed field's usages and callings will also change respectively.
-
There will be children and parents having their desired fields added or removed.
PushDownField
The main function that does the process of pull up field refactoring.
Adds the necessary fields to the subclasses and removes them from the superclass.
Source code in codart\refactorings\pushdown_field.py
class PushDownField:
"""
The main function that does the process of pull up field refactoring.
Adds the necessary fields to the subclasses and removes them from the superclass.
"""
def __init__(self, source_filenames: list,
package_name: str,
superclass_name: str,
field_name: str,
class_names: list = [],
filename_mapping=lambda x: (x[:-5] if x.endswith(".java") else x) + ".java"):
"""
Args:
source_filenames (list): A list of file names to be processed
package_name (str): The name of the package in which the refactoring has to be done \
(contains the superclass)
superclass_name (str): The name of the needed superclass
class_names (list): Name of the classes in which the refactoring has to be done \
(the classes to push down field from)
field_name (str): Name of the field that has to be refactored
filename_mapping (str): Mapping the file's name to the correct format so that it can be processed
Returns:
object (PushDownField): An instance of PushDownField class
"""
self.source_filenames = source_filenames
self.package_name = package_name
self.superclass_name = superclass_name
self.field_name = field_name
self.class_names = class_names
self.filename_mapping = filename_mapping
def pre_condition_check(self, program, superclass):
if self.package_name not in program.packages \
or self.superclass_name not in program.packages[self.package_name].classes \
or self.field_name not in program.packages[self.package_name].classes[self.superclass_name].fields:
return False
for m in superclass.methods:
method: symbol_table.Method = superclass.methods[m]
for item in method.body_local_vars_and_expr_names:
if isinstance(item, symbol_table.ExpressionName):
if ((len(item.dot_separated_identifiers) == 1
and item.dot_separated_identifiers[0] == self.field_name)
or (len(item.dot_separated_identifiers) == 2
and item.dot_separated_identifiers[0] == "this"
and item.dot_separated_identifiers[1] == self.field_name)):
return False
return True
def do_refactor(self):
program = symbol_table.get_program(self.source_filenames, print_status=False)
superclass: symbol_table.Class = program.packages[self.package_name].classes[self.superclass_name]
if not self.pre_condition_check(program, superclass):
print(f"Cannot push-down field from {superclass.name}")
return False
other_derived_classes = []
classes_to_add_to = []
for pn in program.packages:
p: symbol_table.Package = program.packages[pn]
for cn in p.classes:
c: symbol_table.Class = p.classes[cn]
if ((c.superclass_name == self.superclass_name and
c.file_info.has_imported_class(self.package_name, self.superclass_name)) or
(
self.package_name is not None and c.superclass_name == self.package_name + '.' + self.superclass_name)):
# all_derived_classes.append(c)
if len(self.class_names) == 0 or cn in self.class_names:
if self.field_name in c.fields:
print("some classes have same variable")
return False
else:
classes_to_add_to.append(c)
else:
other_derived_classes.append(c)
# Check if the field is used from the superclass or other derived classes
for pn in program.packages:
p: symbol_table.Package = program.packages[pn]
for cn in p.classes:
c: symbol_table.Class = p.classes[cn]
has_imported_superclass = c.file_info.has_imported_class(self.package_name, self.superclass_name)
fields_of_superclass_type_or_others = []
for fn in c.fields:
f: symbol_table.Field = c.fields[fn]
if (f.name == self.field_name and has_imported_superclass) \
or (self.package_name is not None and f.name == (
self.package_name + '.' + self.superclass_name)):
fields_of_superclass_type_or_others.append(f.name)
if any((c.file_info.has_imported_class(o.package_name, o.name) and f.datatype == o.name)
or f.datatype == (o.package_name + '.' + o.name) for o in other_derived_classes):
fields_of_superclass_type_or_others.append(f.name)
for mk in c.methods:
m: symbol_table.Method = c.methods[mk]
local_vars_of_superclass_type_or_others = []
for item in m.body_local_vars_and_expr_names:
if isinstance(item, symbol_table.LocalVariable):
if (item.datatype == self.superclass_name and has_imported_superclass) \
or item.datatype == (self.package_name + '.' + self.superclass_name):
local_vars_of_superclass_type_or_others.append(item.identifier)
if any((c.file_info.has_imported_class(o.package_name, o.name) and item.datatype == o.name)
or item.datatype == (o.package_name + '.' + o.name) for o in other_derived_classes):
local_vars_of_superclass_type_or_others.append(item.identifier)
elif isinstance(item, symbol_table.ExpressionName):
if item.dot_separated_identifiers[-1] == self.field_name \
and (
(len(item.dot_separated_identifiers) == 2)
or (len(item.dot_separated_identifiers) == 3 and item.dot_separated_identifiers[
0] == "this")
) and (
(item.dot_separated_identifiers[
-2] in local_vars_of_superclass_type_or_others and len(
item.dot_separated_identifiers) == 2)
or item.dot_separated_identifiers[-2] in fields_of_superclass_type_or_others
):
return False
rewriter = symbol_table.Rewriter(program, self.filename_mapping)
field = superclass.fields[self.field_name]
if len(field.neighbor_names) == 0:
rewriter.replace(field.get_tokens_info(), "")
# Have to remove the modifiers too, because of the new grammar.
for mod_ctx in field.modifiers_parser_contexts:
rewriter.replace(symbol_table.TokensInfo(mod_ctx), "")
else:
i = field.index_in_variable_declarators
var_ctxs = field.all_variable_declarator_contexts
if i == 0:
to_remove = symbol_table.TokensInfo(var_ctxs[i])
to_remove.stop = symbol_table.TokensInfo(var_ctxs[i + 1]).start - 1 # Include the ',' after it
rewriter.replace(to_remove, "")
else:
to_remove = symbol_table.TokensInfo(var_ctxs[i])
to_remove.start = symbol_table.TokensInfo(var_ctxs[i - 1]).stop + 1 # Include the ',' before it
rewriter.replace(to_remove, "")
is_public = "public" in field.modifiers
is_protected = "protected" in field.modifiers
modifier = ("public " if is_public else ("protected " if is_protected else ""))
for c in classes_to_add_to:
c_body_start = symbol_table.TokensInfo(c.parser_context.classBody())
c_body_start.stop = c_body_start.start # Start and stop both point to the '{'
rewriter.insert_after(c_body_start,
(
"\n " + modifier + field.datatype + " "
+ self.field_name
+ ((" = " + field.initializer) if field.initializer is not None else "")
+ ";"
)
)
rewriter.apply()
return True
__init__(self, source_filenames, package_name, superclass_name, field_name, class_names=[], filename_mapping=<function PushDownField.<lambda> at 0x000001616E4A4700>)
special
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source_filenames |
list |
A list of file names to be processed |
required |
package_name |
str |
The name of the package in which the refactoring has to be done (contains the superclass) |
required |
superclass_name |
str |
The name of the needed superclass |
required |
class_names |
list |
Name of the classes in which the refactoring has to be done (the classes to push down field from) |
[] |
field_name |
str |
Name of the field that has to be refactored |
required |
filename_mapping |
str |
Mapping the file's name to the correct format so that it can be processed |
<function PushDownField.<lambda> at 0x000001616E4A4700> |
Returns:
Type | Description |
---|---|
object (PushDownField) |
An instance of PushDownField class |
Source code in codart\refactorings\pushdown_field.py
def __init__(self, source_filenames: list,
package_name: str,
superclass_name: str,
field_name: str,
class_names: list = [],
filename_mapping=lambda x: (x[:-5] if x.endswith(".java") else x) + ".java"):
"""
Args:
source_filenames (list): A list of file names to be processed
package_name (str): The name of the package in which the refactoring has to be done \
(contains the superclass)
superclass_name (str): The name of the needed superclass
class_names (list): Name of the classes in which the refactoring has to be done \
(the classes to push down field from)
field_name (str): Name of the field that has to be refactored
filename_mapping (str): Mapping the file's name to the correct format so that it can be processed
Returns:
object (PushDownField): An instance of PushDownField class
"""
self.source_filenames = source_filenames
self.package_name = package_name
self.superclass_name = superclass_name
self.field_name = field_name
self.class_names = class_names
self.filename_mapping = filename_mapping
main(project_dir, source_package, source_class, field_name, target_classes, *args, **kwargs)
Source code in codart\refactorings\pushdown_field.py
def main(project_dir, source_package, source_class, field_name, target_classes: list, *args, **kwargs):
"""
"""
res = PushDownField(
symbol_table.get_filenames_in_dir(project_dir),
package_name=source_package,
superclass_name=source_class,
field_name=field_name,
class_names=target_classes,
).do_refactor()
if not res:
logger.error("Cannot push-down field")
return False
return True
Push-down field 2
Introduction
The module implements a light-weight version of push-down field refactoring described in pushdown_field.py
.
Pre-conditions:
Todo: Add pre-conditions
Post-conditions:
Todo: Add post-conditions
CutFieldListener (JavaParserLabeledListener)
Removes the field declaration from the parent class.
Source code in codart\refactorings\pushdown_field2.py
class CutFieldListener(JavaParserLabeledListener):
"""
Removes the field declaration from the parent class.
"""
def __init__(self, source_class:str, field_name:str, rewriter: TokenStreamRewriter):
"""
Args:
source_class: (str) Parent's class name.
field_name: (str) Field's name.
rewriter (TokenStreamRewriter): ANTLR's token stream rewriter.
Returns:
field_content (CutFieldListener): The full string of field declaration
"""
self.source_class = source_class
self.field_name = field_name
self.rewriter = rewriter
self.field_content = ""
self.import_statements = ""
self.detected_field = False
self.is_source_class = False
def enterClassDeclaration(self, ctx: JavaParserLabeled.ClassDeclarationContext):
class_name = ctx.IDENTIFIER().getText()
if class_name == self.source_class:
self.is_source_class = True
def exitClassDeclaration(self, ctx: JavaParserLabeled.ClassDeclarationContext):
class_name = ctx.IDENTIFIER().getText()
if self.is_source_class and class_name == self.source_class:
self.is_source_class = False
def enterImportDeclaration(self, ctx: JavaParserLabeled.ImportDeclarationContext):
statement = self.rewriter.getText(
program_name=self.rewriter.DEFAULT_PROGRAM_NAME,
start=ctx.start.tokenIndex,
stop=ctx.stop.tokenIndex
)
self.import_statements += statement + "\n"
def exitVariableDeclaratorId(self, ctx: JavaParserLabeled.VariableDeclaratorIdContext):
variable_name = ctx.IDENTIFIER().getText()
if self.is_source_class:
if variable_name == self.field_name:
self.detected_field = True
def exitClassBodyDeclaration2(self, ctx: JavaParserLabeled.ClassBodyDeclaration2Context):
if self.detected_field and self.is_source_class:
self.field_content = self.rewriter.getText(
program_name=self.rewriter.DEFAULT_PROGRAM_NAME,
start=ctx.start.tokenIndex,
stop=ctx.stop.tokenIndex
)
self.rewriter.delete(
program_name=self.rewriter.DEFAULT_PROGRAM_NAME,
from_idx=ctx.start.tokenIndex,
to_idx=ctx.stop.tokenIndex
)
self.detected_field = False
__init__(self, source_class, field_name, rewriter)
special
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source_class |
str |
(str) Parent's class name. |
required |
field_name |
str |
(str) Field's name. |
required |
rewriter |
TokenStreamRewriter |
ANTLR's token stream rewriter. |
required |
Returns:
Type | Description |
---|---|
field_content (CutFieldListener) |
The full string of field declaration |
Source code in codart\refactorings\pushdown_field2.py
def __init__(self, source_class:str, field_name:str, rewriter: TokenStreamRewriter):
"""
Args:
source_class: (str) Parent's class name.
field_name: (str) Field's name.
rewriter (TokenStreamRewriter): ANTLR's token stream rewriter.
Returns:
field_content (CutFieldListener): The full string of field declaration
"""
self.source_class = source_class
self.field_name = field_name
self.rewriter = rewriter
self.field_content = ""
self.import_statements = ""
self.detected_field = False
self.is_source_class = False
PasteFieldListener (JavaParserLabeledListener)
Inserts field declaration to children classes.
Source code in codart\refactorings\pushdown_field2.py
class PasteFieldListener(JavaParserLabeledListener):
"""
Inserts field declaration to children classes.
"""
def __init__(self, source_class, field_content, import_statements, rewriter: TokenStreamRewriter):
"""
Args:
source_class: Child class name.
field_content: Full string of the field declaration.
rewriter: Antlr's token stream rewriter.
Returns:
object (PasteFieldListener): An instance of PasteFieldListener class
"""
self.source_class = source_class
self.rewriter = rewriter
self.field_content = field_content
self.import_statements = import_statements
self.is_source_class = False
def enterClassDeclaration(self, ctx: JavaParserLabeled.ClassDeclarationContext):
class_name = ctx.IDENTIFIER().getText()
if class_name == self.source_class:
self.is_source_class = True
def exitClassDeclaration(self, ctx: JavaParserLabeled.ClassDeclarationContext):
class_name = ctx.IDENTIFIER().getText()
if self.is_source_class and class_name == self.source_class:
self.is_source_class = False
def exitPackageDeclaration(self, ctx: JavaParserLabeled.PackageDeclarationContext):
self.rewriter.insertAfter(
program_name=self.rewriter.DEFAULT_PROGRAM_NAME,
index=ctx.stop.tokenIndex,
text="\n" + self.import_statements
)
def enterClassBody(self, ctx: JavaParserLabeled.ClassBodyContext):
if self.is_source_class:
self.rewriter.insertAfter(
index=ctx.start.tokenIndex,
text="\n\t" + self.field_content
)
__init__(self, source_class, field_content, import_statements, rewriter)
special
Parameters:
Name | Type | Description | Default |
---|---|---|---|
source_class |
Child class name. |
required | |
field_content |
Full string of the field declaration. |
required | |
rewriter |
TokenStreamRewriter |
Antlr's token stream rewriter. |
required |
Returns:
Type | Description |
---|---|
object (PasteFieldListener) |
An instance of PasteFieldListener class |
Source code in codart\refactorings\pushdown_field2.py
def __init__(self, source_class, field_content, import_statements, rewriter: TokenStreamRewriter):
"""
Args:
source_class: Child class name.
field_content: Full string of the field declaration.
rewriter: Antlr's token stream rewriter.
Returns:
object (PasteFieldListener): An instance of PasteFieldListener class
"""
self.source_class = source_class
self.rewriter = rewriter
self.field_content = field_content
self.import_statements = import_statements
self.is_source_class = False
main(udb_path=None, source_package=None, source_class=None, field_name=None, target_classes=None, *args, **kwargs)
The main API for push-down field refactoring
Source code in codart\refactorings\pushdown_field2.py
def main(udb_path=None, source_package=None, source_class=None, field_name=None, target_classes: list = None, *args,
**kwargs):
"""
The main API for push-down field refactoring
"""
if udb_path is None:
db = und.open(codart.config.UDB_PATH)
else:
db = und.open(udb_path)
source_class_ent = None
source_class_ents = db.lookup(f"{source_package}.{source_class}", "Class")
if len(source_class_ents) == 0:
logger.error(f"Cannot find source class: {source_class}")
db.close()
return False
else:
for ent in source_class_ents:
if ent.simplename() == source_class:
source_class_ent = ent
break
if source_class_ent is None:
logger.error(f"Cannot find source class: {source_class}")
db.close()
return False
fields = db.lookup(f"{source_package}.{source_class}.{field_name}", "Variable")
if fields is None or len(fields) == 0:
logger.error(f"Cannot find field to pushdown: {field_name}")
db.close()
return False
else:
field_ent = fields[0]
target_class_ents_files = []
target_class_ents_simplenames = []
for ref in source_class_ent.refs("Extendby"):
if ref.ent().simplename() not in target_classes:
logger.error("Target classes are not children classes")
db.close()
return False
target_class_ents_files.append(ref.ent().parent().longname())
target_class_ents_simplenames.append(ref.ent().simplename())
for ref in field_ent.refs("Useby, Setby"):
if ref.file().simplename().split(".")[0] in target_classes:
continue
else:
logger.error("Field has dependencies.")
db.close()
return False
source_class_file = source_class_ent.parent().longname()
db.close()
# Remove field from source class
listener = parse_and_walk(
file_path=source_class_file,
listener_class=CutFieldListener,
has_write=True,
source_class=source_class,
field_name=field_name,
debug=False
)
# Insert field in children classes
for i, target_class_file in enumerate(target_class_ents_files):
parse_and_walk(
file_path=target_class_file,
listener_class=PasteFieldListener,
has_write=True,
source_class=target_class_ents_simplenames[i],
field_content=listener.field_content,
import_statements=listener.import_statements,
debug=False
)
# db.close()
return True