github.com/getgauge/gauge@v1.6.9/refactor/refactor.go (about)

     1  /*----------------------------------------------------------------
     2   *  Copyright (c) ThoughtWorks, Inc.
     3   *  Licensed under the Apache License, Version 2.0
     4   *  See LICENSE in the project root for license information.
     5   *----------------------------------------------------------------*/
     6  
     7  /*
     8     Given old and new step gives the filenames of specification, concepts and files in code changed.
     9  
    10     Refactoring Flow:
    11  	- Refactor specs and concepts in memory
    12  	- Checks if it is a concept or not
    13  	- In case of concept - writes to file and skips the runner
    14  	- 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)
    15  	- Refactors the text(changes param positions ect) and sends it to runner to refactor implementations.
    16  */
    17  package refactor
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/getgauge/gauge-proto/go/gauge_messages"
    26  	"github.com/getgauge/gauge/config"
    27  	"github.com/getgauge/gauge/formatter"
    28  	"github.com/getgauge/gauge/gauge"
    29  	"github.com/getgauge/gauge/logger"
    30  	"github.com/getgauge/gauge/parser"
    31  	"github.com/getgauge/gauge/runner"
    32  	"github.com/getgauge/gauge/util"
    33  )
    34  
    35  type rephraseRefactorer struct {
    36  	oldStep   *gauge.Step
    37  	newStep   *gauge.Step
    38  	isConcept bool
    39  	runner    runner.Runner
    40  }
    41  
    42  type refactoringResult struct {
    43  	Success            bool
    44  	SpecsChanged       []*gauge_messages.FileChanges
    45  	ConceptsChanged    []*gauge_messages.FileChanges
    46  	RunnerFilesChanged []*gauge_messages.FileChanges
    47  	Errors             []string
    48  	Warnings           []string
    49  }
    50  
    51  func (res *refactoringResult) String() string {
    52  	o := `Refactoring result from gauge:
    53  Specs changed        : %s
    54  Concepts changed     : %s
    55  Source files changed : %s
    56  Warnings             : %s
    57  `
    58  	return fmt.Sprintf(o, res.specFilesChanged(), res.conceptFilesChanged(), res.runnerFilesChanged(), res.Warnings)
    59  }
    60  
    61  func (refactoringResult *refactoringResult) appendWarnings(warnings []*parser.Warning) {
    62  	if refactoringResult.Warnings == nil {
    63  		refactoringResult.Warnings = make([]string, 0)
    64  	}
    65  	for _, warning := range warnings {
    66  		refactoringResult.Warnings = append(refactoringResult.Warnings, warning.Message)
    67  	}
    68  }
    69  
    70  func (refactoringResult *refactoringResult) AllFilesChanged() []string {
    71  	filesChanged := make([]string, 0)
    72  	filesChanged = append(filesChanged, refactoringResult.specFilesChanged()...)
    73  	filesChanged = append(filesChanged, refactoringResult.conceptFilesChanged()...)
    74  	filesChanged = append(filesChanged, refactoringResult.runnerFilesChanged()...)
    75  	return filesChanged
    76  }
    77  
    78  func (refactoringResult *refactoringResult) conceptFilesChanged() []string {
    79  	filesChanged := make([]string, 0)
    80  	for _, fileChange := range refactoringResult.ConceptsChanged {
    81  		filesChanged = append(filesChanged, fileChange.FileName)
    82  	}
    83  	return filesChanged
    84  }
    85  
    86  func (refactoringResult *refactoringResult) specFilesChanged() []string {
    87  	filesChanged := make([]string, 0)
    88  	for _, filesChange := range refactoringResult.SpecsChanged {
    89  		filesChanged = append(filesChanged, filesChange.FileName)
    90  	}
    91  	return filesChanged
    92  }
    93  
    94  func (refactoringResult *refactoringResult) runnerFilesChanged() []string {
    95  	filesChanged := make([]string, 0)
    96  	for _, fileChange := range refactoringResult.RunnerFilesChanged {
    97  		filesChanged = append(filesChanged, fileChange.FileName)
    98  	}
    99  	return filesChanged
   100  }
   101  
   102  func (refactoringResult *refactoringResult) WriteToDisk() {
   103  	if !refactoringResult.Success {
   104  		return
   105  	}
   106  	// fileChange.FileContent need not be deprecated. To save the refactored file, it is much simpler and less error prone
   107  	// to replace the file with new content, rather than parsing again and replacing specific lines.
   108  	for _, fileChange := range refactoringResult.SpecsChanged {
   109  		util.SaveFile(fileChange.FileName, fileChange.FileContent, true) //nolint:staticcheck
   110  	}
   111  	for _, fileChange := range refactoringResult.ConceptsChanged {
   112  		util.SaveFile(fileChange.FileName, fileChange.FileContent, true) //nolint:staticcheck
   113  	}
   114  }
   115  
   116  // GetRefactoringChanges given an old step and new step gives the list of steps that need to be changed to perform refactoring.
   117  // It also provides the changes to be made on the implementation files.
   118  func GetRefactoringChanges(oldStep, newStep string, r runner.Runner, specDirs []string, saveToDisk bool) *refactoringResult {
   119  	if newStep == oldStep {
   120  		return &refactoringResult{Success: true}
   121  	}
   122  	agent, errs := getRefactorAgent(oldStep, newStep, r)
   123  
   124  	if len(errs) > 0 {
   125  		var messages []string
   126  		for _, err := range errs {
   127  			messages = append(messages, err.Error())
   128  		}
   129  		return rephraseFailure(messages...)
   130  	}
   131  	result, specs, conceptDictionary := parseSpecsAndConcepts(specDirs)
   132  	if !result.Success {
   133  		return result
   134  	}
   135  
   136  	refactorResult := agent.getRefactoringChangesFor(specs, conceptDictionary, saveToDisk)
   137  	refactorResult.Warnings = append(refactorResult.Warnings, result.Warnings...)
   138  	return refactorResult
   139  }
   140  
   141  func parseSpecsAndConcepts(specDirs []string) (*refactoringResult, []*gauge.Specification, *gauge.ConceptDictionary) {
   142  	result := &refactoringResult{Success: true, Errors: make([]string, 0), Warnings: make([]string, 0)}
   143  
   144  	var specs []*gauge.Specification
   145  	var specParseResults []*parser.ParseResult
   146  
   147  	for _, dir := range specDirs {
   148  		specFiles := util.GetSpecFiles([]string{filepath.Join(config.ProjectRoot, dir)})
   149  		specSlice, specParseResultsSlice := parser.ParseSpecFiles(specFiles, &gauge.ConceptDictionary{}, gauge.NewBuildErrors())
   150  		specs = append(specs, specSlice...)
   151  		specParseResults = append(specParseResults, specParseResultsSlice...)
   152  	}
   153  
   154  	addErrorsAndWarningsToRefactoringResult(result, specParseResults...)
   155  	if !result.Success {
   156  		return result, nil, nil
   157  	}
   158  
   159  	conceptDictionary, parseResult, err := parser.CreateConceptsDictionary()
   160  	if err != nil {
   161  		return rephraseFailure(err.Error()), nil, nil
   162  	}
   163  	addErrorsAndWarningsToRefactoringResult(result, parseResult)
   164  	return result, specs, conceptDictionary
   165  }
   166  
   167  func rephraseFailure(errs ...string) *refactoringResult {
   168  	return &refactoringResult{Success: false, Errors: errs}
   169  }
   170  
   171  func addErrorsAndWarningsToRefactoringResult(refactorResult *refactoringResult, parseResults ...*parser.ParseResult) {
   172  	for _, parseResult := range parseResults {
   173  		if !parseResult.Ok {
   174  			refactorResult.Success = false
   175  			refactorResult.Errors = append(refactorResult.Errors, parseResult.Errors()...)
   176  		}
   177  		refactorResult.appendWarnings(parseResult.Warnings)
   178  	}
   179  }
   180  
   181  func (agent *rephraseRefactorer) getRefactoringChangesFor(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary, saveToDisk bool) *refactoringResult {
   182  	specsRefactored, conceptFilesRefactored := agent.rephraseInSpecsAndConcepts(&specs, conceptDictionary)
   183  	result := agent.refactorStepImplementations(saveToDisk)
   184  	if !result.Success {
   185  		return result
   186  	}
   187  	result.SpecsChanged, result.ConceptsChanged = getFileChanges(specs, conceptDictionary, specsRefactored, conceptFilesRefactored)
   188  	return result
   189  }
   190  
   191  func (agent *rephraseRefactorer) refactorStepImplementations(shouldSaveChanges bool) *refactoringResult {
   192  	result := &refactoringResult{Success: false, Errors: make([]string, 0), Warnings: make([]string, 0)}
   193  	if !agent.isConcept {
   194  		stepName, err, warning := agent.getStepNameFromRunner(agent.runner)
   195  		if err != nil {
   196  			result.Errors = append(result.Errors, err.Error())
   197  			return result
   198  		}
   199  		if warning == nil {
   200  			runnerFilesChanged, err := agent.requestRunnerForRefactoring(agent.runner, stepName, shouldSaveChanges)
   201  			if err != nil {
   202  				result.Errors = append(result.Errors, fmt.Sprintf("Cannot perform refactoring: %s", err))
   203  				return result
   204  			}
   205  			result.RunnerFilesChanged = runnerFilesChanged
   206  		} else {
   207  			result.Warnings = append(result.Warnings, warning.Message)
   208  		}
   209  	}
   210  	result.Success = true
   211  	return result
   212  }
   213  
   214  func (agent *rephraseRefactorer) rephraseInSpecsAndConcepts(specs *[]*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) (map[*gauge.Specification][]*gauge.StepDiff, map[string][]*gauge.StepDiff) {
   215  	specsRefactored := make(map[*gauge.Specification][]*gauge.StepDiff)
   216  	conceptsRefactored := make(map[string][]*gauge.StepDiff)
   217  	orderMap := agent.createOrderOfArgs()
   218  	for _, spec := range *specs {
   219  		diffs, isRefactored := spec.RenameSteps(agent.oldStep, agent.newStep, orderMap)
   220  		if isRefactored {
   221  			specsRefactored[spec] = diffs
   222  		}
   223  	}
   224  	isConcept := false
   225  	for _, concept := range conceptDictionary.ConceptsMap {
   226  		isRefactored := false
   227  		for _, item := range concept.ConceptStep.Items {
   228  			if item.Kind() == gauge.StepKind {
   229  				diff, isRefactored := item.(*gauge.Step).Rename(agent.oldStep, agent.newStep, isRefactored, orderMap, &isConcept)
   230  				if isRefactored {
   231  					conceptsRefactored[concept.FileName] = append(conceptsRefactored[concept.FileName], diff)
   232  				}
   233  			}
   234  		}
   235  	}
   236  	agent.isConcept = isConcept
   237  	return specsRefactored, conceptsRefactored
   238  }
   239  
   240  func (agent *rephraseRefactorer) createOrderOfArgs() map[int]int {
   241  	orderMap := make(map[int]int, len(agent.newStep.Args))
   242  	for i, arg := range agent.newStep.Args {
   243  		orderMap[i] = SliceIndex(len(agent.oldStep.Args), func(i int) bool { return agent.oldStep.Args[i].String() == arg.String() })
   244  	}
   245  	return orderMap
   246  }
   247  
   248  // SliceIndex gives the index of the args.
   249  func SliceIndex(limit int, predicate func(i int) bool) int {
   250  	for i := 0; i < limit; i++ {
   251  		if predicate(i) {
   252  			return i
   253  		}
   254  	}
   255  	return -1
   256  }
   257  
   258  func getRefactorAgent(oldStepText, newStepText string, r runner.Runner) (*rephraseRefactorer, []parser.ParseError) {
   259  	specParser := new(parser.SpecParser)
   260  	stepTokens, errs := specParser.GenerateTokens("* "+oldStepText+"\n"+"*"+newStepText, "")
   261  	if len(errs) > 0 {
   262  		return nil, errs
   263  	}
   264  
   265  	steps := make([]*gauge.Step, 0)
   266  	for _, stepToken := range stepTokens {
   267  		step, parseRes := parser.CreateStepUsingLookup(stepToken, nil, "")
   268  		if parseRes != nil && len(parseRes.ParseErrors) > 0 {
   269  			return nil, parseRes.ParseErrors
   270  		}
   271  		steps = append(steps, step)
   272  	}
   273  	return &rephraseRefactorer{oldStep: steps[0], newStep: steps[1], runner: r}, []parser.ParseError{}
   274  }
   275  
   276  func (agent *rephraseRefactorer) requestRunnerForRefactoring(testRunner runner.Runner, stepName string, shouldSaveChanges bool) ([]*gauge_messages.FileChanges, error) {
   277  	refactorRequest, err := agent.createRefactorRequest(stepName, shouldSaveChanges)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	refactorResponse := agent.sendRefactorRequest(testRunner, refactorRequest)
   282  	var runnerError error
   283  	if !refactorResponse.GetSuccess() {
   284  		logger.Errorf(false, "Refactoring error response from runner: %v", refactorResponse.GetError())
   285  		runnerError = errors.New(refactorResponse.GetError())
   286  	}
   287  	if len(refactorResponse.GetFileChanges()) == 0 {
   288  		for _, file := range refactorResponse.GetFilesChanged() {
   289  			refactorResponse.FileChanges = append(refactorResponse.FileChanges, &gauge_messages.FileChanges{FileName: file})
   290  		}
   291  	}
   292  	return refactorResponse.GetFileChanges(), runnerError
   293  }
   294  
   295  func (agent *rephraseRefactorer) sendRefactorRequest(testRunner runner.Runner, refactorRequest *gauge_messages.Message) *gauge_messages.RefactorResponse {
   296  	response, err := testRunner.ExecuteMessageWithTimeout(refactorRequest)
   297  	if err != nil {
   298  		return &gauge_messages.RefactorResponse{Success: false, Error: err.Error()}
   299  	}
   300  	return response.GetRefactorResponse()
   301  }
   302  
   303  //Todo: Check for inline tables
   304  func (agent *rephraseRefactorer) createRefactorRequest(stepName string, shouldSaveChanges bool) (*gauge_messages.Message, error) {
   305  	oldStepValue, err := agent.getStepValueFor(agent.oldStep, stepName)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	orderMap := agent.createOrderOfArgs()
   310  	newStepName := agent.generateNewStepName(oldStepValue.Args, orderMap)
   311  	newStepValue, err := parser.ExtractStepValueAndParams(newStepName, false)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	oldProtoStepValue := gauge.ConvertToProtoStepValue(oldStepValue)
   316  	newProtoStepValue := gauge.ConvertToProtoStepValue(newStepValue)
   317  	return &gauge_messages.Message{MessageType: gauge_messages.Message_RefactorRequest,
   318  		RefactorRequest: &gauge_messages.RefactorRequest{
   319  			OldStepValue:   oldProtoStepValue,
   320  			NewStepValue:   newProtoStepValue,
   321  			ParamPositions: agent.createParameterPositions(orderMap),
   322  			SaveChanges:    shouldSaveChanges,
   323  		},
   324  	}, nil
   325  }
   326  
   327  func (agent *rephraseRefactorer) generateNewStepName(args []string, orderMap map[int]int) string {
   328  	agent.newStep.PopulateFragments()
   329  	paramIndex := 0
   330  	for _, fragment := range agent.newStep.Fragments {
   331  		if fragment.GetFragmentType() == gauge_messages.Fragment_Parameter {
   332  			if orderMap[paramIndex] != -1 {
   333  				fragment.GetParameter().Value = args[orderMap[paramIndex]]
   334  			}
   335  			paramIndex++
   336  		}
   337  	}
   338  	return parser.ConvertToStepText(agent.newStep.Fragments)
   339  }
   340  
   341  func (agent *rephraseRefactorer) getStepNameFromRunner(r runner.Runner) (string, error, *parser.Warning) {
   342  	stepNameMessage := &gauge_messages.Message{MessageType: gauge_messages.Message_StepNameRequest, StepNameRequest: &gauge_messages.StepNameRequest{StepValue: agent.oldStep.Value}}
   343  	responseMessage, err := r.ExecuteMessageWithTimeout(stepNameMessage)
   344  
   345  	if err != nil {
   346  		return "", err, nil
   347  	}
   348  	if !(responseMessage.GetStepNameResponse().GetIsStepPresent()) {
   349  		return "", nil, &parser.Warning{Message: fmt.Sprintf("Step implementation not found: %s", agent.oldStep.LineText)}
   350  	}
   351  	if responseMessage.GetStepNameResponse().GetHasAlias() {
   352  		return "", fmt.Errorf("steps with aliases : '%s' cannot be refactored", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil
   353  	}
   354  	if responseMessage.GetStepNameResponse().GetIsExternal() {
   355  		return "", fmt.Errorf("external step: Cannot refactor '%s' is in external project or library", strings.Join(responseMessage.GetStepNameResponse().GetStepName(), "', '")), nil
   356  	}
   357  	return responseMessage.GetStepNameResponse().GetStepName()[0], nil, nil
   358  }
   359  
   360  func (agent *rephraseRefactorer) createParameterPositions(orderMap map[int]int) []*gauge_messages.ParameterPosition {
   361  	paramPositions := make([]*gauge_messages.ParameterPosition, 0)
   362  	for k, v := range orderMap {
   363  		paramPositions = append(paramPositions, &gauge_messages.ParameterPosition{NewPosition: int32(k), OldPosition: int32(v)})
   364  	}
   365  	return paramPositions
   366  }
   367  
   368  func (agent *rephraseRefactorer) getStepValueFor(step *gauge.Step, stepName string) (*gauge.StepValue, error) {
   369  	return parser.ExtractStepValueAndParams(stepName, false)
   370  }
   371  
   372  func createDiffs(diffs []*gauge.StepDiff) []*gauge_messages.TextDiff {
   373  	textDiffs := []*gauge_messages.TextDiff{}
   374  	for _, diff := range diffs {
   375  		newtext := strings.TrimSpace(formatter.FormatStep(diff.NewStep))
   376  		if diff.IsConcept && !diff.OldStep.InConcept() {
   377  			newtext = strings.Replace(newtext, "*", "#", -1)
   378  		}
   379  		oldFragments := util.GetLinesFromText(strings.TrimSpace(formatter.FormatStep(&diff.OldStep)))
   380  		d := &gauge_messages.TextDiff{
   381  			Span: &gauge_messages.Span{
   382  				Start:     int64(diff.OldStep.LineNo),
   383  				StartChar: 0,
   384  				End:       int64(diff.OldStep.LineNo + len(oldFragments) - 1),
   385  				EndChar:   int64(len(oldFragments[len(oldFragments)-1])),
   386  			},
   387  			Content: newtext,
   388  		}
   389  		textDiffs = append(textDiffs, d)
   390  	}
   391  	return textDiffs
   392  }
   393  
   394  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) {
   395  	specFiles := []*gauge_messages.FileChanges{}
   396  	conceptFiles := []*gauge_messages.FileChanges{}
   397  	for _, spec := range specs {
   398  		if stepDiffs, ok := specsRefactored[spec]; ok {
   399  			formatted := formatter.FormatSpecification(spec)
   400  			specFiles = append(specFiles, &gauge_messages.FileChanges{FileName: spec.FileName, FileContent: formatted, Diffs: createDiffs(stepDiffs)})
   401  		}
   402  	}
   403  	conceptMap := formatter.FormatConcepts(conceptDictionary)
   404  	for file, diffs := range conceptsRefactored {
   405  		conceptFiles = append(conceptFiles, &gauge_messages.FileChanges{FileName: file, FileContent: conceptMap[file], Diffs: createDiffs(diffs)})
   406  	}
   407  	return specFiles, conceptFiles
   408  }