github.com/yaman/gauge@v0.1.1-0.20181130223501-7b7ae34faf33/refactor/refactor.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  /*
    19     Given old and new step gives the filenames of specification, concepts and files in code changed.
    20  
    21     Refactoring Flow:
    22  	- Refactor specs and concepts in memory
    23  	- Checks if it is a concept or not
    24  	- In case of concept - writes to file and skips the runner
    25  	- If its not a concept (its a step) - need to know the text, so makes a call to runner to get the text(step name)
    26  	- Refactors the text(changes param positions ect) and sends it to runner to refactor implementations.
    27  */
    28  package refactor
    29  
    30  import (
    31  	"errors"
    32  	"fmt"
    33  	"os"
    34  	"path/filepath"
    35  	"strings"
    36  
    37  	"github.com/getgauge/gauge/config"
    38  	"github.com/getgauge/gauge/formatter"
    39  	"github.com/getgauge/gauge/gauge"
    40  	"github.com/getgauge/gauge/gauge_messages"
    41  	"github.com/getgauge/gauge/logger"
    42  	"github.com/getgauge/gauge/parser"
    43  	"github.com/getgauge/gauge/runner"
    44  	"github.com/getgauge/gauge/util"
    45  )
    46  
    47  type rephraseRefactorer struct {
    48  	oldStep   *gauge.Step
    49  	newStep   *gauge.Step
    50  	isConcept bool
    51  	runner    runner.Runner
    52  }
    53  
    54  type refactoringResult struct {
    55  	Success            bool
    56  	SpecsChanged       []*gauge_messages.FileChanges
    57  	ConceptsChanged    []*gauge_messages.FileChanges
    58  	RunnerFilesChanged []*gauge_messages.FileChanges
    59  	Errors             []string
    60  	Warnings           []string
    61  }
    62  
    63  func (refactoringResult *refactoringResult) String() string {
    64  	result := fmt.Sprintf("Refactoring result from gauge:\n")
    65  	result += fmt.Sprintf("Specs changed        : %s\n", refactoringResult.specFilesChanged())
    66  	result += fmt.Sprintf("Concepts changed     : %s\n", refactoringResult.conceptFilesChanged())
    67  	result += fmt.Sprintf("Source files changed : %s\n", refactoringResult.runnerFilesChanged())
    68  	result += fmt.Sprintf("Warnings             : %s\n", refactoringResult.Warnings)
    69  	return result
    70  }
    71  
    72  func (refactoringResult *refactoringResult) appendWarnings(warnings []*parser.Warning) {
    73  	if refactoringResult.Warnings == nil {
    74  		refactoringResult.Warnings = make([]string, 0)
    75  	}
    76  	for _, warning := range warnings {
    77  		refactoringResult.Warnings = append(refactoringResult.Warnings, warning.Message)
    78  	}
    79  }
    80  
    81  func (refactoringResult *refactoringResult) AllFilesChanged() []string {
    82  	filesChanged := make([]string, 0)
    83  	filesChanged = append(filesChanged, refactoringResult.specFilesChanged()...)
    84  	filesChanged = append(filesChanged, refactoringResult.conceptFilesChanged()...)
    85  	filesChanged = append(filesChanged, refactoringResult.runnerFilesChanged()...)
    86  	return filesChanged
    87  }
    88  
    89  func (refactoringResult *refactoringResult) conceptFilesChanged() []string {
    90  	filesChanged := make([]string, 0)
    91  	for _, fileChange := range refactoringResult.ConceptsChanged {
    92  		filesChanged = append(filesChanged, fileChange.FileName)
    93  	}
    94  	return filesChanged
    95  }
    96  
    97  func (refactoringResult *refactoringResult) specFilesChanged() []string {
    98  	filesChanged := make([]string, 0)
    99  	for _, filesChange := range refactoringResult.SpecsChanged {
   100  		filesChanged = append(filesChanged, filesChange.FileName)
   101  	}
   102  	return filesChanged
   103  }
   104  
   105  func (refactoringResult *refactoringResult) runnerFilesChanged() []string {
   106  	filesChanged := make([]string, 0)
   107  	for _, fileChange := range refactoringResult.RunnerFilesChanged {
   108  		filesChanged = append(filesChanged, fileChange.FileName)
   109  	}
   110  	return filesChanged
   111  }
   112  
   113  // PerformRephraseRefactoring given an old step and new step refactors specs and concepts in memory and if its a concept writes to file
   114  // else invokes runner to get the step name and refactors the step and sends it to runner to refactor implementation.
   115  func PerformRephraseRefactoring(oldStep, newStep string, startChan *runner.StartChannels, specDirs []string) *refactoringResult {
   116  	defer killRunner(startChan)
   117  	if newStep == oldStep {
   118  		return &refactoringResult{Success: true}
   119  	}
   120  	agent, errs := getRefactorAgent(oldStep, newStep, nil)
   121  
   122  	if len(errs) > 0 {
   123  		var messages []string
   124  		for _, err := range errs {
   125  			messages = append(messages, err.Error())
   126  		}
   127  		return rephraseFailure(messages...)
   128  	}
   129  	result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs)
   130  	if !result.Success {
   131  		return result
   132  	}
   133  
   134  	refactorResult := agent.performRefactoringOn(specs, conceptDictionary, startChan)
   135  	refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...)
   136  	return refactorResult
   137  }
   138  
   139  // GetRefactoringChanges given an old step and new step gives the list of steps that need to be changed to perform refactoring.
   140  // It also provides the changes to be made on the implementation files.
   141  func GetRefactoringChanges(oldStep, newStep string, runner runner.Runner, specDirs []string) *refactoringResult {
   142  	if newStep == oldStep {
   143  		return &refactoringResult{Success: true}
   144  	}
   145  	agent, errs := getRefactorAgent(oldStep, newStep, runner)
   146  
   147  	if len(errs) > 0 {
   148  		var messages []string
   149  		for _, err := range errs {
   150  			messages = append(messages, err.Error())
   151  		}
   152  		return rephraseFailure(messages...)
   153  	}
   154  	result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs)
   155  	if !result.Success {
   156  		return result
   157  	}
   158  
   159  	refactorResult := agent.getRefactoringChangesFor(specs, conceptDictionary)
   160  	refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...)
   161  	return refactorResult
   162  }
   163  
   164  func parseSpecsAndConcepts(specDirs []string) (*refactoringResult, []*gauge.Specification, *gauge.ConceptDictionary) {
   165  	result := &refactoringResult{Success: true, Errors: make([]string, 0), Warnings: make([]string, 0)}
   166  
   167  	var specs []*gauge.Specification
   168  	var specParseResults []*parser.ParseResult
   169  
   170  	for _, dir := range specDirs {
   171  		specFiles := util.GetSpecFiles([]string{filepath.Join(config.ProjectRoot, dir)})
   172  		specSlice, specParseResultsSlice := parser.ParseSpecFiles(specFiles, &gauge.ConceptDictionary{}, gauge.NewBuildErrors())
   173  		specs = append(specs, specSlice...)
   174  		specParseResults = append(specParseResults, specParseResultsSlice...)
   175  	}
   176  
   177  	addErrorsAndWarningsToRefactoringResult(result, specParseResults...)
   178  	if !result.Success {
   179  		return result, nil, nil
   180  	}
   181  
   182  	conceptDictionary, parseResult, err := parser.CreateConceptsDictionary()
   183  	if err != nil {
   184  		return rephraseFailure(err.Error()), nil, nil
   185  	}
   186  	addErrorsAndWarningsToRefactoringResult(result, parseResult)
   187  	return result, specs, conceptDictionary
   188  }
   189  
   190  func killRunner(startChan *runner.StartChannels) {
   191  	startChan.KillChan <- true
   192  }
   193  
   194  func rephraseFailure(errors ...string) *refactoringResult {
   195  	return &refactoringResult{Success: false, Errors: errors}
   196  }
   197  
   198  func addErrorsAndWarningsToRefactoringResult(refactorResult *refactoringResult, parseResults ...*parser.ParseResult) {
   199  	for _, parseResult := range parseResults {
   200  		if !parseResult.Ok {
   201  			refactorResult.Success = false
   202  			for _, err := range parseResult.Errors() {
   203  				refactorResult.Errors = append(refactorResult.Errors, err)
   204  			}
   205  		}
   206  		refactorResult.appendWarnings(parseResult.Warnings)
   207  	}
   208  }
   209  
   210  func (agent *rephraseRefactorer) performRefactoringOn(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, startChan *runner.StartChannels) *refactoringResult {
   211  	specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary)
   212  	var runner runner.Runner
   213  	select {
   214  	case runner = <-startChan.RunnerChan:
   215  	case err := <-startChan.ErrorChan:
   216  		logger.Debugf(true, "Cannot perform refactoring: Unable to connect to runner."+err.Error())
   217  		return &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)}
   218  	}
   219  	agent.runner = runner
   220  	result := agent.refactorStepImplementations(true)
   221  	if !result.Success {
   222  		return result
   223  	}
   224  	result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored)
   225  	writeFileChangesToDisk(result)
   226  	return result
   227  }
   228  
   229  func writeFileChangesToDisk(result *refactoringResult) {
   230  	for _, fileChange := range result.SpecsChanged {
   231  		util.SaveFile(fileChange.FileName, fileChange.FileContent, true)
   232  	}
   233  	for _, fileChange := range result.ConceptsChanged {
   234  		util.SaveFile(fileChange.FileName, fileChange.FileContent, true)
   235  	}
   236  }
   237  
   238  func (agent *rephraseRefactorer) getRefactoringChangesFor(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) *refactoringResult {
   239  	specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary)
   240  	result := agent.refactorStepImplementations(false)
   241  	if !result.Success {
   242  		return result
   243  	}
   244  	result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored)
   245  	return result
   246  }
   247  
   248  func (agent *rephraseRefactorer) refactorStepImplementations(shouldSaveChanges bool) *refactoringResult {
   249  	result := &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)}
   250  	if !agent.isConcept {
   251  		stepName, err, warning := agent.getStepNameFromRunner(agent.runner)
   252  		if err != nil {
   253  			result.Errors = append(result.Errors, err.Error())
   254  			return result
   255  		}
   256  		if warning == nil {
   257  			runnerFilesChanged, err := agent.requestRunnerForRefactoring(agent.runner, stepName, shouldSaveChanges)
   258  			if err != nil {
   259  				result.Errors = append(result.Errors, fmt.Sprintf("Cannot perform refactoring: %s", err))
   260  				return result
   261  			}
   262  			result.RunnerFilesChanged = runnerFilesChanged
   263  		} else {
   264  			result.Warnings = append(result.Warnings, warning.Message)
   265  		}
   266  	}
   267  	result.Success = true
   268  	return result
   269  }
   270  
   271  func (agent *rephraseRefactorer) rephraseInSpecsAndConcepts(specs *[]*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) (map[*gauge.Specification][]*gauge.StepDiff, map[string][]*gauge.StepDiff) {
   272  	specsRefactored := make(map[*gauge.Specification][]*gauge.StepDiff, 0)
   273  	conceptsRefactored := make(map[string][]*gauge.StepDiff, 0)
   274  	orderMap := agent.createOrderOfArgs()
   275  	for _, spec := range *specs {
   276  		diffs, isRefactored := spec.RenameSteps(*agent.oldStep, *agent.newStep, orderMap)
   277  		if isRefactored {
   278  			specsRefactored[spec] = diffs
   279  		}
   280  	}
   281  	isConcept := false
   282  	for _, concept := range conceptDictionary.ConceptsMap {
   283  		isRefactored := false
   284  		for _, item := range concept.ConceptStep.Items {
   285  			if item.Kind() == gauge.StepKind {
   286  				diff, isRefactored := item.(*gauge.Step).Rename(*agent.oldStep, *agent.newStep, isRefactored, orderMap, &isConcept)
   287  				if isRefactored {
   288  					conceptsRefactored[concept.FileName] = append(conceptsRefactored[concept.FileName], diff)
   289  				}
   290  			}
   291  		}
   292  	}
   293  	agent.isConcept = isConcept
   294  	return specsRefactored, conceptsRefactored
   295  }
   296  
   297  func (agent *rephraseRefactorer) createOrderOfArgs() map[int]int {
   298  	orderMap := make(map[int]int, len(agent.newStep.Args))
   299  	for i, arg := range agent.newStep.Args {
   300  		orderMap[i] = SliceIndex(len(agent.oldStep.Args), func(i int) bool { return agent.oldStep.Args[i].String() == arg.String() })
   301  	}
   302  	return orderMap
   303  }
   304  
   305  // SliceIndex gives the index of the args.
   306  func SliceIndex(limit int, predicate func(i int) bool) int {
   307  	for i := 0; i < limit; i++ {
   308  		if predicate(i) {
   309  			return i
   310  		}
   311  	}
   312  	return -1
   313  }
   314  
   315  func getRefactorAgent(oldStepText, newStepText string, runner runner.Runner) (*rephraseRefactorer, []parser.ParseError) {
   316  	specParser := new(parser.SpecParser)
   317  	stepTokens, errs := specParser.GenerateTokens("* "+oldStepText+"\n"+"*"+newStepText, "")
   318  	if len(errs) > 0 {
   319  		return nil, errs
   320  	}
   321  
   322  	steps := make([]*gauge.Step, 0)
   323  	for _, stepToken := range stepTokens {
   324  		step, parseRes := parser.CreateStepUsingLookup(stepToken, nil, "")
   325  		if parseRes != nil && len(parseRes.ParseErrors) > 0 {
   326  			return nil, parseRes.ParseErrors
   327  		}
   328  		steps = append(steps, step)
   329  	}
   330  	return &rephraseRefactorer{oldStep: steps[0], newStep: steps[1], runner: runner}, []parser.ParseError{}
   331  }
   332  
   333  func (agent *rephraseRefactorer) requestRunnerForRefactoring(testRunner runner.Runner, stepName string, shouldSaveChanges bool) ([]*gauge_messages.FileChanges, error) {
   334  	refactorRequest, err := agent.createRefactorRequest(testRunner, stepName, shouldSaveChanges)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  	refactorResponse := agent.sendRefactorRequest(testRunner, refactorRequest)
   339  	var runnerError error
   340  	if !refactorResponse.GetSuccess() {
   341  		logger.Errorf(false, "Refactoring error response from runner: %v", refactorResponse.GetError())
   342  		runnerError = errors.New(refactorResponse.GetError())
   343  	}
   344  	if len(refactorResponse.GetFileChanges()) == 0 {
   345  		for _, file := range refactorResponse.GetFilesChanged() {
   346  			refactorResponse.FileChanges = append(refactorResponse.FileChanges, &gauge_messages.FileChanges{FileName: file})
   347  		}
   348  	}
   349  	return refactorResponse.GetFileChanges(), runnerError
   350  }
   351  
   352  func (agent *rephraseRefactorer) sendRefactorRequest(testRunner runner.Runner, refactorRequest *gauge_messages.Message) *gauge_messages.RefactorResponse {
   353  	response, err := testRunner.ExecuteMessageWithTimeout(refactorRequest)
   354  	if err != nil {
   355  		return &gauge_messages.RefactorResponse{Success: false, Error: err.Error()}
   356  	}
   357  	return response.GetRefactorResponse()
   358  }
   359  
   360  //Todo: Check for inline tables
   361  func (agent *rephraseRefactorer) createRefactorRequest(runner runner.Runner, stepName string, shouldSaveChanges bool) (*gauge_messages.Message, error) {
   362  	oldStepValue, err := agent.getStepValueFor(agent.oldStep, stepName)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	orderMap := agent.createOrderOfArgs()
   367  	newStepName := agent.generateNewStepName(oldStepValue.Args, orderMap)
   368  	newStepValue, err := parser.ExtractStepValueAndParams(newStepName, false)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	oldProtoStepValue := gauge.ConvertToProtoStepValue(oldStepValue)
   373  	newProtoStepValue := gauge.ConvertToProtoStepValue(newStepValue)
   374  	return &gauge_messages.Message{MessageType: gauge_messages.Message_RefactorRequest,
   375  		RefactorRequest: &gauge_messages.RefactorRequest{
   376  			OldStepValue:   oldProtoStepValue,
   377  			NewStepValue:   newProtoStepValue,
   378  			ParamPositions: agent.createParameterPositions(orderMap),
   379  			SaveChanges:    shouldSaveChanges,
   380  		},
   381  	}, nil
   382  }
   383  
   384  func (agent *rephraseRefactorer) generateNewStepName(args []string, orderMap map[int]int) string {
   385  	agent.newStep.PopulateFragments()
   386  	paramIndex := 0
   387  	for _, fragment := range agent.newStep.Fragments {
   388  		if fragment.GetFragmentType() == gauge_messages.Fragment_Parameter {
   389  			if orderMap[paramIndex] != -1 {
   390  				fragment.GetParameter().Value = args[orderMap[paramIndex]]
   391  			}
   392  			paramIndex++
   393  		}
   394  	}
   395  	return parser.ConvertToStepText(agent.newStep.Fragments)
   396  }
   397  
   398  func (agent *rephraseRefactorer) getStepNameFromRunner(runner runner.Runner) (string, error, *parser.Warning) {
   399  	stepNameMessage := &gauge_messages.Message{MessageType: gauge_messages.Message_StepNameRequest, StepNameRequest: &gauge_messages.StepNameRequest{StepValue: agent.oldStep.Value}}
   400  	responseMessage, err := runner.ExecuteMessageWithTimeout(stepNameMessage)
   401  	if err != nil {
   402  		return "", err, nil
   403  	}
   404  	if !(responseMessage.GetStepNameResponse().GetIsStepPresent()) {
   405  		return "", nil, &parser.Warning{Message: fmt.Sprintf("Step implementation not found: %s", agent.oldStep.LineText)}
   406  	}
   407  	if responseMessage.GetStepNameResponse().GetHasAlias() {
   408  		return "", fmt.Errorf("steps with aliases : '%s' cannot be refactored", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil
   409  	}
   410  	return responseMessage.GetStepNameResponse().GetStepName()[0], nil, nil
   411  }
   412  
   413  func (agent *rephraseRefactorer) createParameterPositions(orderMap map[int]int) []*gauge_messages.ParameterPosition {
   414  	paramPositions := make([]*gauge_messages.ParameterPosition, 0)
   415  	for k, v := range orderMap {
   416  		paramPositions = append(paramPositions, &gauge_messages.ParameterPosition{NewPosition: int32(k), OldPosition: int32(v)})
   417  	}
   418  	return paramPositions
   419  }
   420  
   421  func (agent *rephraseRefactorer) getStepValueFor(step *gauge.Step, stepName string) (*gauge.StepValue, error) {
   422  	return parser.ExtractStepValueAndParams(stepName, false)
   423  }
   424  
   425  func createDiffs(diffs []*gauge.StepDiff) []*gauge_messages.TextDiff {
   426  	textDiffs := []*gauge_messages.TextDiff{}
   427  	for _, diff := range diffs {
   428  		newtext := strings.TrimSpace(formatter.FormatStep(diff.NewStep))
   429  		if diff.IsConcept && !diff.OldStep.InConcept() {
   430  			newtext = strings.Replace(newtext, "*", "#", -1)
   431  		}
   432  		oldFragments := util.GetLinesFromText(strings.TrimSpace(formatter.FormatStep(&diff.OldStep)))
   433  		d := &gauge_messages.TextDiff{
   434  			Span: &gauge_messages.Span{
   435  				Start:     int64(diff.OldStep.LineNo),
   436  				StartChar: 0,
   437  				End:       int64(diff.OldStep.LineNo + len(oldFragments) - 1),
   438  				EndChar:   int64(len(oldFragments[len(oldFragments)-1])),
   439  			},
   440  			Content: newtext,
   441  		}
   442  		textDiffs = append(textDiffs, d)
   443  	}
   444  	return textDiffs
   445  }
   446  
   447  func getFileChanges(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, specsRefactored map[*gauge.Specification][]*gauge.StepDiff, conceptsRefactored map[string][]*gauge.StepDiff) ([]*gauge_messages.FileChanges, []*gauge_messages.FileChanges) {
   448  	specFiles := []*gauge_messages.FileChanges{}
   449  	conceptFiles := []*gauge_messages.FileChanges{}
   450  	for _, spec := range specs {
   451  		if stepDiffs, ok := specsRefactored[spec]; ok {
   452  			formatted := formatter.FormatSpecification(spec)
   453  			specFiles = append(specFiles, &gauge_messages.FileChanges{FileName: spec.FileName, FileContent: formatted, Diffs: createDiffs(stepDiffs)})
   454  		}
   455  	}
   456  	conceptMap := formatter.FormatConcepts(conceptDictionary)
   457  	for file, diffs := range conceptsRefactored {
   458  		conceptFiles = append(conceptFiles, &gauge_messages.FileChanges{FileName: file, FileContent: conceptMap[file], Diffs: createDiffs(diffs)})
   459  	}
   460  	return specFiles, conceptFiles
   461  }
   462  
   463  func printRefactoringSummary(refactoringResult *refactoringResult) {
   464  	exitCode := 0
   465  	if !refactoringResult.Success {
   466  		exitCode = 1
   467  		for _, err := range refactoringResult.Errors {
   468  			logger.Errorf(true, "%s \n", err)
   469  		}
   470  	}
   471  	for _, warning := range refactoringResult.Warnings {
   472  		logger.Warningf(true, "%s \n", warning)
   473  	}
   474  	logger.Infof(true, "%d specifications changed.\n", len(refactoringResult.specFilesChanged()))
   475  	logger.Infof(true, "%d concepts changed.\n", len(refactoringResult.conceptFilesChanged()))
   476  	logger.Infof(true, "%d files in code changed.\n", len(refactoringResult.RunnerFilesChanged))
   477  	os.Exit(exitCode)
   478  }
   479  
   480  // RefactorSteps performs rephrase refactoring and prints the refactoring summary which includes errors and warnings generated during refactoring and
   481  // files changed during refactoring : specification files, concept files and the implementation files changed.
   482  func RefactorSteps(oldStep, newStep string, startChan *runner.StartChannels, specDirs []string) {
   483  	refactoringResult := PerformRephraseRefactoring(oldStep, newStep, startChan, specDirs)
   484  	printRefactoringSummary(refactoringResult)
   485  }