Skip to content

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:

  1. There should exist a corresponding child and parent in the project.

  2. The field that should be pushed down must be valid.

  3. The user must enter the package's name, class's name and the fields that need to be added.

Post Conditions:

  1. The changed field's usages and callings will also change respectively.

  2. 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