diff --git a/setup.py b/setup.py index d974144b..de73a9ac 100755 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ license='mit', python_requires='>=3.5', install_requires=[ - 'bugzoo>=2.1.12', + 'bugzoo>=2.1.14', 'rooibos>=0.3.0', - 'boggart>=0.1.8', + 'boggart>=0.1.12', 'kaskara>=0.0.1', 'attrs>=17.2.0', 'requests', diff --git a/src/darjeeling/candidate.py b/src/darjeeling/candidate.py index 038f27a2..5670df67 100644 --- a/src/darjeeling/candidate.py +++ b/src/darjeeling/candidate.py @@ -1,6 +1,6 @@ -__all__ = ['Candidate'] +__all__ = ['Candidate', 'all_single_edit_patches'] -from typing import List, Iterator, Dict, FrozenSet +from typing import List, Iterator, Dict, FrozenSet, Iterable import attr from bugzoo.core.patch import Patch @@ -44,3 +44,13 @@ def lines_changed(self, problem: Problem) -> List[FileLine]: lines = [FileLine(loc.filename, loc.start.line) for loc in locations] return lines + + +def all_single_edit_patches(transformations: Iterable[Transformation] + ) -> Iterable[Candidate]: + """ + Returns an iterator over all of the single-edit patches that can be + composed using a provided source of transformations. + """ + for t in transformations: + yield Candidate([t]) # type: ignore diff --git a/src/darjeeling/core.py b/src/darjeeling/core.py index 27cead31..9c07a3a7 100644 --- a/src/darjeeling/core.py +++ b/src/darjeeling/core.py @@ -2,4 +2,4 @@ from boggart.core.replacement import Replacement from boggart.core.location import FileLocationRange, FileLine, Location, \ - LocationRange, FileLocation + LocationRange, FileLocation, FileLineSet diff --git a/src/darjeeling/generator.py b/src/darjeeling/generator.py deleted file mode 100644 index 88276c18..00000000 --- a/src/darjeeling/generator.py +++ /dev/null @@ -1,375 +0,0 @@ -""" -This module provides a number of composable methods for generating code -transformations and candidate patches. -""" -from typing import Iterator, List, Iterable, Tuple, Optional, Type, Dict -import random -import logging -import yaml - -from rooibos import Client as RooibosClient -from rooibos import Match - -from .localization import Localization -from .exceptions import NoImplicatedLines -from .core import FileLocationRange, FileLine, Location, LocationRange -from .problem import Problem -from .snippet import Snippet, SnippetDatabase -from .candidate import Candidate -from .transformation import Transformation, \ - RooibosTransformation, \ - AppendTransformation, \ - DeleteTransformation, \ - ReplaceTransformation - -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - - -class Context(object): - pass - # provide access to files - - -class CandidateGenerator(Iterable): - """ - Candidate generators are used to generate (normally in a lazy fashion) a - stream of candidate patches. For now, candidate generators implement - one-way communications: they do not accept inputs. - """ - def __iter__(self) -> Iterator[Candidate]: - return self - - def __next__(self) -> Candidate: - raise NotImplementedError - - -class TransformationGenerator(Iterable): - """ - Transformation generators are used to provide a stream of source code - transformations. As with candidate generators, transformations are usually - generated in a lazy fashion. - """ - def __iter__(self) -> Iterator[Transformation]: - return self - - def __next__(self) -> Transformation: - raise NotImplementedError - - -class RooibosGenerator(TransformationGenerator): - def __init__(self, - problem: Problem, - snippets: SnippetDatabase, - localization: Localization, - schemas: List[Type[RooibosTransformation]] - ) -> None: - client_rooibos = problem.rooibos - self.__snippets = snippets - self.__problem = problem - size = 0 - tally_by_file = \ - {fn: 0 for fn in localization.files} # type: Dict[str, int] - tally_by_schema = \ - {s: 0 for s in schemas} # type: Dict[Type[RooibosTransformation], int] # noqa: pycodestyle - self.__localization = localization - self.__transformations = \ - {l: {s: [] for s in schemas} for l in localization} # type: Dict[FileLine, Dict[Type[RooibosTransformation], List[Transformation]]] # noqa: pycodestyle - - logger.debug("computing transformations") - for fn in localization.files: - file_contents = problem.sources.read_file(fn) - for schema in schemas: - logger.debug("finding matches of %s in %s", schema.__name__, fn) - tpl_match = schema.match - matches = client_rooibos.matches(file_contents, tpl_match) - for m in matches: - line = FileLine(fn, m.location.start.line) - if line not in localization: - continue - if not schema.is_valid_match(m): - logger.debug("skipping invalid match: %s", m) - continue - transformations = \ - list(self._match_to_transformations(fn, schema, m)) - size += len(transformations) - tally_by_schema[schema] += len(transformations) - tally_by_file[fn] += len(transformations) - self.__transformations[line][schema] += transformations - - # trim redundant parts of transformation map - for line in localization: - for schema in schemas: - if not self.__transformations[line][schema]: - del self.__transformations[line][schema] - if not self.__transformations[line]: - del self.__transformations[line] - - # refine the fault localization to only cover represented lines - lines = list(self.__transformations.keys()) - self.__localization = self.__localization.restricted_to_lines(lines) - logger.info("finished computing transformations: %d transformations across %d lines", # noqa: pycodestyle - size, len(self.__transformations)) - logger.info('tranformations by schema:\n%s', - '\n'.join([' * {}: {}'.format(s.__name__, count) - for (s, count) in tally_by_schema.items()])) - logger.info('tranformations by file:\n%s', - '\n'.join([' * {}: {}'.format(fn, count) - for (fn, count) in tally_by_file.items()])) - - def _match_to_transformations(self, - filename: str, - schema: Type[RooibosTransformation], - match: Match - ) -> List[Transformation]: - loc_start = Location(match.location.start.line, - match.location.start.col) - loc_stop = Location(match.location.stop.line, - match.location.stop.col) - location = FileLocationRange(filename, - LocationRange(loc_start, loc_stop)) - return schema.match_to_transformations(self.__problem, - self.__snippets, - location, - match.environment) - - def __next__(self) -> Transformation: - line = self.__localization.sample() - logger.debug("looking for transformation at %s", line) - operator_to_transformations = self.__transformations[line] - - # choose an operator at random - # if there are no operator choices, discard this line - # if no lines remain, we're finished - try: - op = random.choice(list(operator_to_transformations.keys())) - transformations = operator_to_transformations[op] - except IndexError: - logger.debug("no transformations left at %s", line) - del self.__transformations[line] - try: - self.__localization = self.__localization.without(line) - except NoImplicatedLines: - logger.debug("no transformations left in search space") - raise StopIteration - return self.__next__() - - # choose a transformation at random - # if there are no more transformation choices, discard this operator - # choice - if not transformations: - logger.debug("exhausted all %s transformations at %s", op, line) - del operator_to_transformations[op] - return self.__next__() - return transformations.pop() - - -def all_transformations_in_file( - problem: Problem, - transformation_cls: Type[RooibosTransformation], - filename: str - ) -> Iterator[Transformation]: - client_rooibos = problem.rooibos - file_contents = problem.sources.read_file(filename) - tpl_match = transformation_cls.match - tpl_rewrite = transformation_cls.rewrite - matches = client_rooibos.matches(file_contents, tpl_match) - for m in matches: - args = {} # type: Dict[str, str] # FIXME - location = FileLocationRange(filename, - Location(m.location.start.line, - m.location.start.col), - Location(m.location.stop.line, - m.location.stop.col)) - yield transformation_cls(location, args) # type: ignore - - -class SingleEditPatches(CandidateGenerator): - """ - Provides a stream of single-transformation candidate patches composed using - a provided stream of transformations. - """ - def __init__(self, - transformations: Iterator[Transformation] - ) -> None: - self.__transformations = transformations - - def __next__(self) -> Candidate: - try: - transformation = next(self.__transformations) - except StopIteration: - raise StopIteration - return Candidate([transformation]) # type: ignore - - -class TargetSnippetGenerator(Iterable): - def __init__(self, - targets: Iterator[FileLocationRange], - snippets: SnippetDatabase - ) -> None: - self.__targets = targets - self.__snippets = snippets - self.__current_target = None # type: Optional[FileLocationRange] - self.__snippets_at_target = iter([]) # type: Iterator[Snippet] - - def __iter__(self) -> Iterator[Tuple[FileLocationRange, Snippet]]: - return self - - def __next__(self) -> Tuple[FileLocationRange, Snippet]: - # fetch the next snippet at the current line - # if there are no snippets left at this line, move onto - # the next line. if there are no lines left, stop iterating. - try: - snippet = next(self.__snippets_at_target) - return (self.__current_target, snippet) - except StopIteration: - try: - # TODO use snippet generator here - # - return snippets at current file - # - add use/def restrictions - self.__current_target = next(self.__targets) - self.__snippets_at_target = \ - self.__snippets.in_file(self.__current_target.filename) - return self.__next__() - except StopIteration: - raise StopIteration - - -class DeletionGenerator(TransformationGenerator): - def __init__(self, targets: Iterable[FileLocationRange]) -> None: - """ - Constructs a deletion generator. - """ - self.__targets = reversed(list(targets)) - - def __next__(self) -> Transformation: - """ - Returns the next deletion transformation from this generator. - """ - try: - next_target = next(self.__targets) - except StopIteration: - raise StopIteration - - # TODO add static analysis - # should we delete this line? - # * don't delete declarations - return DeleteTransformation(next_target) - - -class ReplacementGenerator(TransformationGenerator): - """ - Uses a provided snippet database to generate all legal replacement - transformations for a sequence of transformation targets. - """ - def __init__(self, - targets: Iterator[FileLocationRange], - snippets: SnippetDatabase - ) -> None: - self.__generator_target_snippet = \ - TargetSnippetGenerator(targets, snippets) - - def __next__(self) -> Transformation: - try: - target, snippet = next(self.__generator_target_snippet) - except StopIteration: - raise StopIteration - - # TODO additional static analysis goes here - # don't replace line with an equivalent - - return ReplaceTransformation(target, snippet) - - -class AppendGenerator(TransformationGenerator): - def __init__(self, - targets: Iterator[FileLocationRange], - snippets: SnippetDatabase - ) -> None: - self.__generator_target_snippet = \ - TargetSnippetGenerator(targets, snippets) - - def __next__(self) -> Transformation: - try: - target, snippet = next(self.__generator_target_snippet) - except StopIteration: - raise StopIteration - - # TODO additional static analysis goes here - # * don't append after a return - # * don't append after a break statement - - return AppendTransformation(target, snippet) - - -class AllTransformationsAtLine(TransformationGenerator): - """ - Provides a stream of all the possible transformations that can be made at - a single line using a given snippet database. - """ - def __init__(self, - problem: Problem, - line: FileLine, - snippets: SnippetDatabase, - *, - randomize: bool = True - ) -> None: - # transform line to character range - # TODO tidy this hack - char_range = problem.sources.line_to_location_range(line) - - # TODO clean up iterator ugliness - self.__sources = [ - DeletionGenerator(iter([char_range])), - ReplacementGenerator(iter([char_range]), snippets), - AppendGenerator(iter([char_range]), snippets) - ] # type: List[TransformationGenerator] - - # TODO implement randomize - - def __next__(self) -> Transformation: - # TODO random/deterministic - # choose a source at random - try: - source = random.choice(self.__sources) - except IndexError: - raise StopIteration - - # attempt to fetch a transformation from that source - # if the source is exhausted, discard it and try again - try: - return next(source) - except StopIteration: - self.__sources.remove(source) - return self.__next__() - - -# TODO: map from transformation targets to line numbers -class SampleByLocalization(TransformationGenerator): - def __init__(self, - problem: Problem, - localization: Localization, - snippets: SnippetDatabase, - *, - randomize: bool = True - ) -> None: - self.__localization = localization - self.__transformations_by_line = { - line: AllTransformationsAtLine(problem, line, snippets, randomize=randomize) - for line in localization - } - - def __next__(self) -> Transformation: - try: - line = self.__localization.sample() - print("Looking at line: {}".format(line)) - source = self.__transformations_by_line[line] - except ValueError: - raise StopIteration - - try: - return next(source) - except StopIteration: - del self.__transformations_by_line[line] - self.__localization = self.__localization.without(line) - return self.__next__() diff --git a/src/darjeeling/transformation.py b/src/darjeeling/transformation.py index e718a0f6..8e47fa18 100644 --- a/src/darjeeling/transformation.py +++ b/src/darjeeling/transformation.py @@ -2,18 +2,27 @@ This module is responsible for describing concrete transformations to source code files. """ -from typing import List, Iterator, Dict, FrozenSet, Tuple +from typing import List, Iterator, Dict, FrozenSet, Tuple, Iterable, Type +from timeit import default_timer as timer +from concurrent.futures import ThreadPoolExecutor import re import logging import os +import random import attr import rooibos from bugzoo.core.bug import Bug +from kaskara import InsertionPoint +from kaskara import Analysis as KaskaraAnalysis +from rooibos import Match +from .exceptions import NoImplicatedLines +from .localization import Localization from .problem import Problem from .snippet import Snippet, SnippetDatabase -from .core import Replacement, FileLine, FileLocationRange, FileLocation +from .core import Replacement, FileLine, FileLocationRange, FileLocation, \ + FileLineSet, Location, LocationRange logger = logging.getLogger(__name__) @@ -29,8 +38,105 @@ class Transformation(object): Represents a transformation to a source code file. """ def to_replacement(self, problem: Problem) -> Replacement: + """ + Converts a transformation into a concrete source code replacement. + """ raise NotImplementedError + @classmethod + def all_at_lines(cls, + problem: Problem, + snippets: SnippetDatabase, + lines: List[FileLine], + *, + threads: int = 1 + ) -> Dict[FileLine, Iterator['Transformation']]: + """ + Returns a dictionary from lines to streams of all the possible + transformations of this type that can be performed at that line. + """ + raise NotImplementedError + + +def sample_by_localization_and_type(problem: Problem, + snippets: SnippetDatabase, + localization: Localization, + schemas: List[Type[Transformation]], + *, + eager: bool = False, + randomize: bool = False, + threads: int = 1 + ) -> Iterator[Transformation]: + """ + Returns an iterator that samples transformations at the different lines + contained within the fault localization in accordance to the probability + distribution defined by their suspiciousness scores. + """ + lines = list(localization) # type: List[FileLine] + try: + schema_to_transformations_by_line = { + s: s.all_at_lines(problem, snippets, lines, threads=threads) + for s in schemas + } # type: Dict[Type[Transformation], Dict[FileLine, Iterator[Transformation]]] # noqa: pycodestyle + logger.debug("built schema->line->transformations map") + except Exception: + logger.exception("failed to build schema->line->transformations map") + raise + + try: + line_to_transformations_by_schema = { + line: {sc: schema_to_transformations_by_line[sc].get(line, iter([])) for sc in schemas} # noqa: pycodestyle + for line in lines + } # type: Dict[FileLine, Dict[Type[Transformation], Iterator[Transformation]]] # noqa: pycodestyle + logger.debug("built line->schema->transformations map") + except Exception: + logger.exception("failed to build line->schema->transformations map") + raise + + # TODO add an optional eager step + + def sample(localization: Localization) -> Iterator[Transformation]: + while True: + line = localization.sample() + logger.debug("finding transformation at line: %s", line) + transformations_by_schema = line_to_transformations_by_schema[line] + + if not transformations_by_schema: + logger.debug("no transformations left at %s", line) + del line_to_transformations_by_schema[line] + try: + localization = localization.without(line) + except NoImplicatedLines: + logger.debug("no transformations left in search space") + raise StopIteration + continue + + schema = random.choice(list(transformations_by_schema.keys())) + transformations = transformations_by_schema[schema] + logger.debug("generating transformation using %s at %s", + schema.__name__, line) + + # attempt to fetch the next transformation for the line and schema + # if none are left, we remove the schema choice + try: + t = next(transformations) + logger.debug("sampled transformation: %s", t) + yield t + except StopIteration: + logger.debug("no %s left at %s", schema.__name__, line) + try: + del transformations_by_schema[schema] + logger.debug("removed entry for schema %s at line %s", + schema.__name__, line) + except Exception: + logger.exception( + "failed to remove entry for %s at %s.\nchoices: %s", + schema.__name__, line, + [s.__name__ for s in transformations_by_schema.keys()]) + raise + + yield from sample(localization) + class RooibosTransformationMeta(type): def __new__(metacls: type, name: str, bases, dikt): @@ -58,6 +164,97 @@ class RooibosTransformation(Transformation, metaclass=RooibosTransformationMeta) arguments = attr.ib(type=FrozenSet[Tuple[str, str]], # TODO replace with FrozenDict converter=lambda args: frozenset(args.items())) # type: ignore # noqa: pycodestyle + @classmethod + def matches_in_file(cls, + problem: Problem, + filename: str + ) -> List[Match]: + """ + Returns an iterator over all of the matches of this transformation's + schema in a given file. + """ + client_rooibos = problem.rooibos + file_contents = problem.sources.read_file(filename) + logger.debug("finding matches of %s in %s", cls.__name__, filename) + time_start = timer() + matches = list(client_rooibos.matches(file_contents, cls.match)) + time_taken = timer() - time_start + logger.debug("found %d matches of %s in %s (took %.3f seconds)", + len(matches), cls.__name__, filename, time_taken) + return matches + + @classmethod + def all_at_lines(cls, + problem: Problem, + snippets: SnippetDatabase, + lines: List[FileLine], + *, + threads: int = 1 + ) -> Dict[FileLine, Iterator[Transformation]]: + file_to_matches = {} # type: Dict[str, List[Match]] + filenames = FileLineSet.from_iter(lines).files + logger.debug("finding all matches of %s in files: %s", + cls.__name__, filenames) + # FIXME compute in parallel + threads = 8 + with ThreadPoolExecutor(max_workers=threads) as executor: + file_to_matches = dict( + executor.map(lambda f: (f, cls.matches_in_file(problem, f)), + filenames)) + """ + for filename in filenames: + file_to_matches[filename] = cls.matches_in_file(problem, filename) + """ + + num_matches = 0 + line_to_matches = {} # type: Dict[FileLine, List[Match]] + for (filename, matches_in_file) in file_to_matches.items(): + for match in matches_in_file: + line = FileLine(filename, match.location.start.line) + + # ignore matches at out-of-scope lines + if line not in lines: + continue + + # ignore invalid matches + if not cls.is_valid_match(match): + continue + + num_matches += 1 + if line not in line_to_matches: + line_to_matches[line] = [] + line_to_matches[line].append(match) + logger.debug("found %d matches of %s across all lines", + num_matches, cls.__name__) + + def matches_at_line_to_transformations(line: FileLine, + matches: Iterable[Match], + ) -> Iterator[Transformation]: + """ + Converts a stream of matches at a given line into a stream of + transformations. + """ + filename = line.filename + for match in matches: + logger.debug("transforming match [%s] to transformations", + match) + loc_start = Location(match.location.start.line, + match.location.start.col) + loc_stop = Location(match.location.stop.line, + match.location.stop.col) + location = FileLocationRange(filename, + LocationRange(loc_start, loc_stop)) + yield from cls.match_to_transformations(problem, + snippets, + location, + match.environment) + + line_to_transformations = { + line: matches_at_line_to_transformations(line, matches_at_line) + for (line, matches_at_line) in line_to_matches.items() + } # type: Dict[FileLine, Iterator[Transformation]] + return line_to_transformations + # FIXME need to use abstract properties @property def rewrite(self) -> str: @@ -78,6 +275,7 @@ def is_valid_match(cls, match: rooibos.Match) -> bool: return True # FIXME automagically generate + # FIXME return an Iterable @classmethod def match_to_transformations(cls, problem: Problem, @@ -89,134 +287,156 @@ def match_to_transformations(cls, return [cls(location, args)] # type: ignore -class InsertVoidFunctionCall(RooibosTransformation): - match = ";\n" - rewrite = ";\n:[1]();\n" +@attr.s(frozen=True) +class InsertStatement(Transformation): + location = attr.ib(type=FileLocation) + statement = attr.ib(type=Snippet) - @classmethod - def is_valid_match(cls, match: rooibos.Match) -> bool: - # TODO must be inside a function - return True + def to_replacement(self, problem: Problem) -> Replacement: + # FIXME will this work? + r = FileLocationRange(self.location.filename, + self.location.location, + self.location.location) + return Replacement(r, self.statement.content) @classmethod - def match_to_transformations(cls, - problem: Problem, - snippets: SnippetDatabase, - location: FileLocationRange, - environment: rooibos.Environment - ) -> List[Transformation]: - # don't insert into small functions + def all_at_lines(cls, + problem: Problem, + snippets: SnippetDatabase, + lines: List[FileLine], + *, + threads: int = 1 + ) -> Dict[FileLine, Iterator[Transformation]]: + return {line: cls.all_at_line(problem, snippets, line) + for line in lines} - # don't insert macros? - - # don't insert after a return statement (or a break?) - # FIXME improve handling of filenames - line_previous = FileLine(location.filename, location.start.line) - line_previous_content = \ - problem.sources.read_line(line_previous) - if ' return ' in line_previous_content: - return [] - - # TODO find all unique insertion points - - # find appropriate void functions - transformations = [] # type: List[Transformation] - for snippet in snippets.in_file(location.filename): - if snippet.kind != 'void-call': - continue - t = cls(location, {'1': snippet.content}) - transformations.append(t) - return transformations - - -class InsertConditionalReturn(RooibosTransformation): - match = ";\n" - rewrite = ";\nif(:[1]){return;}\n" + @classmethod + def all_at_line(cls, + problem: Problem, + snippets: SnippetDatabase, + line: FileLine + ) -> Iterator[Transformation]: + """ + Returns an iterator over all of the possible transformations of this + kind that can be performed at a given line. + """ + if problem.analysis is None: + logger.warning("cannot determine statement insertions: no Kaskara analysis found") # noqa: pycodestyle + yield from [] + return + + points = problem.analysis.insertions.at_line(line) # type: Iterator[InsertionPoint] # noqa: pycodestyle + for point in points: + yield from cls.all_at_point(problem, snippets, point) @classmethod - def is_valid_match(cls, match: rooibos.Match) -> bool: - # TODO must be inside a void function + def should_insert_at_location(cls, + problem: Problem, + location: FileLocation + ) -> bool: + """ + Determines whether an insertion of this kind should be made at a given + location. + """ + if not problem.analysis: + return True + if not problem.analysis.is_inside_function(location): + return False return True @classmethod - def match_to_transformations(cls, - problem: Problem, - snippets: SnippetDatabase, - location: FileLocationRange, - environment: rooibos.Environment - ) -> List[Transformation]: - # TODO contains_return - # don't insert after a return statement (or a break?) - line_previous = FileLine(location.filename, location.start.line) - line_previous_content = \ - problem.sources.read_line(line_previous) - if ' return ' in line_previous_content: - return [] - - # TODO find all unique insertion points - - # only insert into void functions - if problem.analysis: - filename = os.path.join(problem.bug.source_dir, location.filename) - loc_start = FileLocation(filename, location.start) - if not problem.analysis.is_inside_void_function(loc_start): - return [] - - # find appropriate if guards - transformations = [] # type: List[Transformation] - for snippet in snippets: - if snippet.kind != 'guard': - continue - if all(l.filename != location.filename for l in snippet.locations): - continue - t = cls(location, {'1': snippet.content}) - transformations.append(t) - return transformations + def viable_snippets(cls, + problem: Problem, + snippets: SnippetDatabase, + point: InsertionPoint + ) -> Iterator[Snippet]: + """ + Returns an iterator over the set of snippets that can be used as + viable insertions at a given insertion point. + """ + filename = point.location.filename + viable = snippets.in_file(filename) + yield from snippets + + @classmethod + def all_at_point(cls, + problem: Problem, + snippets: SnippetDatabase, + point: InsertionPoint + ) -> Iterator[Transformation]: + """ + Returns an iterator over all of the transformations of this kind that + can be performed at a given insertion point. + """ + location = point.location + if not cls.should_insert_at_location(problem, location): + return + for snippet in cls.viable_snippets(problem, snippets, point): + yield cls(location, snippet) + + +#class InsertVoidFunctionCall(InsertStatement): +# @classmethod +# def match_to_transformations(cls, +# problem: Problem, +# snippets: SnippetDatabase, +# location: FileLocationRange, +# environment: rooibos.Environment +# ) -> List[Transformation]: +# # find appropriate void functions +# transformations = [] # type: List[Transformation] +# for snippet in snippets.in_file(location.filename): +# if snippet.kind != 'void-call': +# continue +# t = cls(location, {'1': snippet.content}) +# transformations.append(t) +# return transformations + + +class InsertConditionalReturn(InsertStatement): + @classmethod + def should_insert_at_location(cls, + problem: Problem, + location: FileLocation + ) -> bool: + if not super().should_insert_at_location: + return False + if not problem.analysis: + return True + return problem.analysis.is_inside_void_function(location) + @classmethod + def viable_snippets(cls, + problem: Problem, + snippets: SnippetDatabase, + point: InsertionPoint + ) -> Iterator[Snippet]: + for snippet in super().viable_snippets(problem, snippets, point): + if snippet.kind == 'guarded-return': + yield snippet -class InsertConditionalBreak(RooibosTransformation): - match = ";\n" - rewrite = ";\nif(:[1]){break;}\n" +class InsertConditionalBreak(InsertStatement): @classmethod - def is_valid_match(cls, match: rooibos.Match) -> bool: - # TODO must be inside a loop - return True + def should_insert_at_location(cls, + problem: Problem, + location: FileLocation + ) -> bool: + if not super().should_insert_at_location: + return False + if not problem.analysis: + return True + return problem.analysis.is_inside_loop(location) @classmethod - def match_to_transformations(cls, - problem: Problem, - snippets: SnippetDatabase, - location: FileLocationRange, - environment: rooibos.Environment - ) -> List[Transformation]: - # TODO contains_return - # don't insert after a return statement (or a break?) - line_previous = FileLine(location.filename, location.start.line) - line_previous_content = \ - problem.sources.read_line(line_previous) - if ' return ' in line_previous_content: - return [] - - # only insert into loops - if problem.analysis: - filename = os.path.join(problem.bug.source_dir, location.filename) - loc_start = FileLocation(filename, location.start) - if not problem.analysis.is_inside_loop(loc_start): - return [] - - # TODO find all unique insertion points - - # find appropriate if guards - transformations = [] # type: List[Transformation] - for snippet in snippets: - if snippet.kind != 'guard': - continue - if all(l.filename != location.filename for l in snippet.locations): - continue - t = cls(location, {'1': snippet.content}) - transformations.append(t) - return transformations + def viable_snippets(cls, + problem: Problem, + snippets: SnippetDatabase, + point: InsertionPoint + ) -> Iterator[Snippet]: + for snippet in super().viable_snippets(problem, snippets, point): + if snippet.kind == 'guarded-break': + yield snippet class ApplyTransformation(RooibosTransformation): diff --git a/src/darjeeling/version.py b/src/darjeeling/version.py index 1c98a23a..850505a3 100644 --- a/src/darjeeling/version.py +++ b/src/darjeeling/version.py @@ -1 +1 @@ -__version__ = '0.1.9' +__version__ = '0.1.10'