github.com/getgauge/gauge@v1.6.9/api/lang/diagnostics.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  package lang
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"sync"
    13  
    14  	"github.com/getgauge/common"
    15  	gm "github.com/getgauge/gauge-proto/go/gauge_messages"
    16  	"github.com/getgauge/gauge/gauge"
    17  	"github.com/getgauge/gauge/parser"
    18  	"github.com/getgauge/gauge/util"
    19  	"github.com/getgauge/gauge/validation"
    20  	"github.com/sourcegraph/go-langserver/pkg/lsp"
    21  	"github.com/sourcegraph/jsonrpc2"
    22  )
    23  
    24  // Diagnostics lock ensures only one goroutine publishes diagnostics at a time.
    25  var diagnosticsLock sync.Mutex
    26  
    27  // isInQueue ensures that only one other goroutine waits for the diagnostic lock.
    28  // Since diagnostics are published for all files, multiple threads need not wait to publish diagnostics.
    29  var isInQueue = false
    30  
    31  func publishDiagnostics(ctx context.Context, conn jsonrpc2.JSONRPC2) {
    32  	defer recoverPanic(nil)
    33  	if !isInQueue {
    34  		isInQueue = true
    35  
    36  		diagnosticsLock.Lock()
    37  		defer diagnosticsLock.Unlock()
    38  
    39  		isInQueue = false
    40  
    41  		diagnosticsMap, err := getDiagnostics()
    42  		if err != nil {
    43  			logError(nil, "Unable to publish diagnostics, error : %s", err.Error())
    44  			return
    45  		}
    46  		for uri, diagnostics := range diagnosticsMap {
    47  			err := publishDiagnostic(uri, diagnostics, conn, ctx)
    48  			if err != nil {
    49  				logError(nil, "Unable to publish diagnostics for %s, error : %s", uri, err.Error())
    50  			}
    51  		}
    52  	}
    53  }
    54  
    55  func publishDiagnostic(uri lsp.DocumentURI, diagnostics []lsp.Diagnostic, conn jsonrpc2.JSONRPC2, ctx context.Context) error {
    56  	params := lsp.PublishDiagnosticsParams{URI: uri, Diagnostics: diagnostics}
    57  	return conn.Notify(ctx, "textDocument/publishDiagnostics", params)
    58  }
    59  
    60  func getDiagnostics() (map[lsp.DocumentURI][]lsp.Diagnostic, error) {
    61  	diagnostics := make(map[lsp.DocumentURI][]lsp.Diagnostic)
    62  	conceptDictionary, err := validateConcepts(diagnostics)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	if err = validateSpecs(conceptDictionary, diagnostics); err != nil {
    67  		return nil, err
    68  	}
    69  	return diagnostics, nil
    70  }
    71  
    72  func createValidationDiagnostics(errors []error, diagnostics map[lsp.DocumentURI][]lsp.Diagnostic) {
    73  	for _, err := range errors {
    74  		uri := util.ConvertPathToURI(err.(validation.StepValidationError).FileName())
    75  		s := err.(validation.StepValidationError).Step()
    76  		d := createDiagnostic(uri, err.(validation.StepValidationError).Message(), s.LineNo-1, s.LineSpanEnd-1, 1)
    77  		if err.(validation.StepValidationError).ErrorType() == gm.StepValidateResponse_STEP_IMPLEMENTATION_NOT_FOUND {
    78  			d.Code = err.(validation.StepValidationError).Suggestion()
    79  		}
    80  		diagnostics[uri] = append(diagnostics[uri], d)
    81  	}
    82  }
    83  
    84  func validateSpecifications(specs []*gauge.Specification, conceptDictionary *gauge.ConceptDictionary) []error {
    85  	if lRunner.runner == nil {
    86  		return []error{}
    87  	}
    88  	vErrs := validation.NewValidator(specs, lRunner.runner, conceptDictionary).Validate()
    89  	return validation.FilterDuplicates(vErrs)
    90  }
    91  
    92  func validateSpecs(conceptDictionary *gauge.ConceptDictionary, diagnostics map[lsp.DocumentURI][]lsp.Diagnostic) error {
    93  	specFiles := util.GetSpecFiles(util.GetSpecDirs())
    94  	specs := make([]*gauge.Specification, 0)
    95  	for _, specFile := range specFiles {
    96  		uri := util.ConvertPathToURI(specFile)
    97  		if _, ok := diagnostics[uri]; !ok {
    98  			diagnostics[uri] = make([]lsp.Diagnostic, 0)
    99  		}
   100  		content, err := getContentFromFileOrDisk(specFile)
   101  		if err != nil {
   102  			return fmt.Errorf("unable to read file %s", err)
   103  		}
   104  		spec, res, err := new(parser.SpecParser).Parse(content, conceptDictionary, specFile)
   105  		if err != nil {
   106  			return err
   107  		}
   108  		createDiagnostics(res, diagnostics)
   109  		if res.Ok {
   110  			specs = append(specs, spec)
   111  		}
   112  	}
   113  	createValidationDiagnostics(validateSpecifications(specs, conceptDictionary), diagnostics)
   114  	return nil
   115  }
   116  
   117  func validateConcepts(diagnostics map[lsp.DocumentURI][]lsp.Diagnostic) (*gauge.ConceptDictionary, error) {
   118  	conceptFiles := util.GetConceptFiles()
   119  	conceptDictionary := gauge.NewConceptDictionary()
   120  	for _, conceptFile := range conceptFiles {
   121  		uri := util.ConvertPathToURI(conceptFile)
   122  		if _, ok := diagnostics[uri]; !ok {
   123  			diagnostics[uri] = make([]lsp.Diagnostic, 0)
   124  		}
   125  		content, err := getContentFromFileOrDisk(conceptFile)
   126  		if err != nil {
   127  			return nil, fmt.Errorf("unable to read file %s", err)
   128  		}
   129  		cpts, pRes := new(parser.ConceptParser).Parse(content, conceptFile)
   130  		pErrs, err := parser.AddConcept(cpts, conceptFile, conceptDictionary) // nolint
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  		pRes.ParseErrors = append(pRes.ParseErrors, pErrs...)
   135  		createDiagnostics(pRes, diagnostics)
   136  	}
   137  	createDiagnostics(parser.ValidateConcepts(conceptDictionary), diagnostics)
   138  	return conceptDictionary, nil
   139  }
   140  
   141  func createDiagnostics(res *parser.ParseResult, diagnostics map[lsp.DocumentURI][]lsp.Diagnostic) {
   142  	for _, err := range res.ParseErrors {
   143  		uri := util.ConvertPathToURI(err.FileName)
   144  		diagnostics[uri] = append(diagnostics[uri], createDiagnostic(uri, err.Message, err.LineNo-1, err.SpanEnd-1, 1))
   145  	}
   146  	for _, warning := range res.Warnings {
   147  		uri := util.ConvertPathToURI(warning.FileName)
   148  		diagnostics[uri] = append(diagnostics[uri], createDiagnostic(uri, warning.Message, warning.LineNo-1, warning.LineSpanEnd-1, 2))
   149  	}
   150  }
   151  
   152  func createDiagnostic(uri lsp.DocumentURI, message string, line int, lineEnd int, severity lsp.DiagnosticSeverity) lsp.Diagnostic {
   153  	endChar := 10000
   154  	if isOpen(uri) {
   155  		endChar = len(getLine(uri, line))
   156  	}
   157  	return lsp.Diagnostic{
   158  		Range: lsp.Range{
   159  			Start: lsp.Position{Line: line, Character: 0},
   160  			End:   lsp.Position{Line: lineEnd, Character: endChar},
   161  		},
   162  		Message:  message,
   163  		Severity: severity,
   164  	}
   165  }
   166  
   167  func getContentFromFileOrDisk(fileName string) (string, error) {
   168  	uri := util.ConvertPathToURI(fileName)
   169  	if isOpen(uri) {
   170  		return getContent(uri), nil
   171  	} else {
   172  		return common.ReadFileContents(fileName)
   173  	}
   174  }