github.com/tiagovtristao/plz@v13.4.0+incompatible/tools/build_langserver/langserver/utils.go (about)

     1  package langserver
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"github.com/thought-machine/please/src/core"
     7  	"fmt"
     8  	"github.com/thought-machine/please/src/fs"
     9  	"io/ioutil"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/thought-machine/please/src/parse/asp"
    17  	"github.com/thought-machine/please/tools/build_langserver/lsp"
    18  )
    19  
    20  var quoteExp = regexp.MustCompile(`(^("|')([^"]|"")*("|'))`)
    21  var strTailExp = regexp.MustCompile(`(("|')([^"]|"")*("|')$)`)
    22  var strExp = regexp.MustCompile(`(^("|')([^"]|"")*("|'))`)
    23  var buildLabelExp = regexp.MustCompile(`("(\/\/|:)(\w+\/?)*(\w+[:]\w*)?"?$)`)
    24  var literalExp = regexp.MustCompile(`(\w*\.?\w*)$`)
    25  
    26  var attrExp = regexp.MustCompile(`(\.[\w]*)$`)
    27  var configAttrExp = regexp.MustCompile(`(CONFIG\.[\w]*)$`)
    28  var strAttrExp = regexp.MustCompile(`((".*"|'.*')\.\w*)$`)
    29  var dictAttrExp = regexp.MustCompile(`({.*}\.\w*)$`)
    30  
    31  // IsURL checks if the documentUri passed has 'file://' prefix
    32  func IsURL(uri lsp.DocumentURI) bool {
    33  	return strings.HasPrefix(string(uri), "file://")
    34  }
    35  
    36  // EnsureURL ensures that the documentURI is a valid path in the filesystem and a valid 'file://' URI
    37  func EnsureURL(uri lsp.DocumentURI, pathType string) (url lsp.DocumentURI, err error) {
    38  	documentPath, err := GetPathFromURL(uri, pathType)
    39  	if err != nil {
    40  		return "", err
    41  	}
    42  
    43  	return lsp.DocumentURI("file://" + documentPath), nil
    44  }
    45  
    46  // GetPathFromURL returns the absolute path of the file which documenURI relates to
    47  // it also checks if the file path is valid
    48  func GetPathFromURL(uri lsp.DocumentURI, pathType string) (documentPath string, err error) {
    49  	var pathFromURL string
    50  	if IsURL(uri) {
    51  		pathFromURL = strings.TrimPrefix(string(uri), "file://")
    52  	} else {
    53  		pathFromURL = string(uri)
    54  	}
    55  
    56  	absPath, err := filepath.Abs(pathFromURL)
    57  	if err != nil {
    58  		return "", err
    59  	}
    60  
    61  	if strings.HasPrefix(absPath, core.RepoRoot) {
    62  		pathType = strings.ToLower(pathType)
    63  		switch pathType {
    64  		case "file":
    65  			if fs.FileExists(absPath) {
    66  				return absPath, nil
    67  			}
    68  			return "", fmt.Errorf("file %s does not exit", pathFromURL)
    69  		case "path":
    70  			if fs.PathExists(absPath) {
    71  				return absPath, nil
    72  			}
    73  			return "", fmt.Errorf("path %s does not exit", pathFromURL)
    74  		default:
    75  			return "", fmt.Errorf(fmt.Sprintf("invalid pathType %s, "+
    76  				"can only be 'file' or 'path'", pathType))
    77  		}
    78  	}
    79  
    80  	return "", fmt.Errorf(fmt.Sprintf("invalid path %s, path must be in repo root: %s", absPath, core.RepoRoot))
    81  }
    82  
    83  // LocalFilesFromURI returns a slices of file path of the files in current directory
    84  // where the document is
    85  func LocalFilesFromURI(uri lsp.DocumentURI) ([]string, error) {
    86  	fp, err := GetPathFromURL(uri, "file")
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	var files []string
    92  
    93  	f, err := ioutil.ReadDir(filepath.Dir(fp))
    94  	fname := filepath.Base(fp)
    95  	for _, i := range f {
    96  		if i.Name() != "." && i.Name() != fname {
    97  			files = append(files, i.Name())
    98  		}
    99  	}
   100  
   101  	return files, err
   102  }
   103  
   104  // PackageLabelFromURI returns a build label of a package
   105  func PackageLabelFromURI(uri lsp.DocumentURI) (string, error) {
   106  	filePath, err := GetPathFromURL(uri, "file")
   107  	if err != nil {
   108  		return "", err
   109  	}
   110  	pathDir := path.Dir(strings.TrimPrefix(filePath, core.RepoRoot))
   111  
   112  	return "/" + pathDir, nil
   113  }
   114  
   115  // ReadFile takes a DocumentURI and reads the file into a slice of string
   116  func ReadFile(ctx context.Context, uri lsp.DocumentURI) ([]string, error) {
   117  	getLines := func(scanner *bufio.Scanner) ([]string, error) {
   118  		var lines []string
   119  
   120  		for scanner.Scan() {
   121  			select {
   122  			case <-ctx.Done():
   123  				log.Info("process cancelled.")
   124  				return nil, nil
   125  			default:
   126  				lines = append(lines, scanner.Text())
   127  			}
   128  		}
   129  
   130  		return lines, scanner.Err()
   131  	}
   132  
   133  	return doIOScan(uri, getLines)
   134  
   135  }
   136  
   137  // GetLineContent returns a []string contraining a single string value respective to position.Line
   138  func GetLineContent(ctx context.Context, uri lsp.DocumentURI, position lsp.Position) ([]string, error) {
   139  	getLine := func(scanner *bufio.Scanner) ([]string, error) {
   140  		lineCount := 0
   141  
   142  		for scanner.Scan() {
   143  			select {
   144  			case <-ctx.Done():
   145  				log.Info("process cancelled.")
   146  				return nil, nil
   147  			default:
   148  				if lineCount == position.Line {
   149  					return []string{scanner.Text()}, nil
   150  				}
   151  				lineCount++
   152  			}
   153  		}
   154  
   155  		return nil, scanner.Err()
   156  	}
   157  
   158  	return doIOScan(uri, getLine)
   159  }
   160  
   161  func doIOScan(uri lsp.DocumentURI, callback func(scanner *bufio.Scanner) ([]string, error)) ([]string, error) {
   162  	filePath, err := GetPathFromURL(uri, "file")
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	file, err := os.Open(filePath)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	defer file.Close()
   173  
   174  	scanner := bufio.NewScanner(file)
   175  
   176  	return callback(scanner)
   177  }
   178  
   179  // TrimQuotes is used to trim the qouted string
   180  // This is usually used to trim the quoted string in BUILD files, such as a BuildLabel
   181  // this will also work for string with any extra characters outside of qoutes
   182  // like so: "//src/core",
   183  func TrimQuotes(str string) string {
   184  	// Regex match the string starts with qoute("),
   185  	// this is so that strings like this(visibility = ["//tools/build_langserver/...", "//src/core"]) won't be matched
   186  	matched := quoteExp.FindString(strings.TrimSpace(str))
   187  	if matched != "" {
   188  		return matched[1 : len(matched)-1]
   189  	}
   190  
   191  	str = strings.Trim(str, `"`)
   192  	str = strings.Trim(str, `'`)
   193  
   194  	return str
   195  }
   196  
   197  // ExtractStrTail extracts the string value from a string,
   198  // **the string value must be at the end of the string passed in**
   199  func ExtractStrTail(str string) string {
   200  	matched := strTailExp.FindString(strings.TrimSpace(str))
   201  
   202  	if matched != "" {
   203  		return matched[1 : len(matched)-1]
   204  	}
   205  
   206  	return ""
   207  }
   208  
   209  // LooksLikeString returns true if the input string looks like a string
   210  func LooksLikeString(str string) bool {
   211  	return mustMatch(strExp, str)
   212  }
   213  
   214  // LooksLikeAttribute returns true if the input string looks like an attribute: "hello".
   215  func LooksLikeAttribute(str string) bool {
   216  	return mustMatch(attrExp, str)
   217  }
   218  
   219  // LooksLikeCONFIGAttr returns true if the input string looks like an attribute of CONFIG object: CONFIG.PLZ_VERSION
   220  func LooksLikeCONFIGAttr(str string) bool {
   221  	return mustMatch(configAttrExp, str)
   222  }
   223  
   224  // LooksLikeStringAttr returns true if the input string looks like an attribute of string: "hello".format()
   225  func LooksLikeStringAttr(str string) bool {
   226  	return mustMatch(strAttrExp, str)
   227  }
   228  
   229  // LooksLikeDictAttr returns true if the input string looks like an attribute of dict
   230  // e.g. {"foo": 1, "bar": "baz"}.keys()
   231  func LooksLikeDictAttr(str string) bool {
   232  	return mustMatch(dictAttrExp, str)
   233  }
   234  
   235  // ExtractBuildLabel extracts build label from a string.
   236  // Beginning of the buildlabel must have a quote
   237  // end of the string must not be anything other than quotes or characters
   238  func ExtractBuildLabel(str string) string {
   239  	matched := buildLabelExp.FindString(strings.TrimSpace(str))
   240  
   241  	return strings.Trim(matched, `"`)
   242  }
   243  
   244  // ExtractLiteral extra a literal expression such as function name, variable name from a content line
   245  func ExtractLiteral(str string) string {
   246  	trimmed := strings.TrimSpace(str)
   247  
   248  	// Ensure the literal we are looking for is not inside of a string
   249  	singleQuotes := regexp.MustCompile(`'`).FindAllString(trimmed, -1)
   250  	doubleQuotes := regexp.MustCompile(`"`).FindAllString(trimmed, -1)
   251  	if len(singleQuotes)%2 != 0 || len(doubleQuotes)%2 != 0 {
   252  		return ""
   253  	}
   254  
   255  	// Get our literal
   256  	matched := literalExp.FindString(trimmed)
   257  	if matched != "" {
   258  		return matched
   259  	}
   260  
   261  	return ""
   262  }
   263  
   264  func mustMatch(re *regexp.Regexp, str string) bool {
   265  	matched := re.FindString(str)
   266  	if matched != "" {
   267  		return true
   268  	}
   269  	return false
   270  }
   271  
   272  // StringInSlice checks if an item is in a string slice
   273  func StringInSlice(strSlice []string, needle string) bool {
   274  	for _, item := range strSlice {
   275  		if item == needle {
   276  			return true
   277  		}
   278  	}
   279  
   280  	return false
   281  }
   282  
   283  // isEmpty checks if the hovered line is empty
   284  func isEmpty(lineContent string, pos lsp.Position) bool {
   285  	return len(lineContent) < pos.Character || strings.TrimSpace(lineContent[:pos.Character]) == ""
   286  }
   287  
   288  // withInRange checks if the input asp.Position from lsp is within the range of the Expression
   289  func withInRange(exprPos asp.Position, exprEndPos asp.Position, pos lsp.Position) bool {
   290  	withInLineRange := pos.Line >= exprPos.Line-1 &&
   291  		pos.Line <= exprEndPos.Line-1
   292  
   293  	withInColRange := pos.Character >= exprPos.Column-1 &&
   294  		pos.Character <= exprEndPos.Column-1
   295  
   296  	onTheSameLine := pos.Line == exprEndPos.Line-1 &&
   297  		pos.Line == exprPos.Line-1
   298  
   299  	if !withInLineRange || (onTheSameLine && !withInColRange) {
   300  		return false
   301  	}
   302  
   303  	if pos.Line == exprPos.Line-1 {
   304  		return pos.Character >= exprPos.Column-1
   305  	}
   306  
   307  	if pos.Line == exprEndPos.Line-1 {
   308  		return pos.Character <= exprEndPos.Column-1
   309  	}
   310  
   311  	return true
   312  }
   313  
   314  func withInRangeLSP(targetPos lsp.Position, targetEndPos lsp.Position, pos lsp.Position) bool {
   315  	start := lspPositionToAsp(targetPos)
   316  	end := lspPositionToAsp(targetEndPos)
   317  
   318  	return withInRange(start, end, pos)
   319  }
   320  
   321  func lspPositionToAsp(pos lsp.Position) asp.Position {
   322  	return asp.Position{
   323  		Line:   pos.Line + 1,
   324  		Column: pos.Character + 1,
   325  	}
   326  }
   327  
   328  func aspPositionToLsp(pos asp.Position) lsp.Position {
   329  	return lsp.Position{
   330  		Line:      pos.Line - 1,
   331  		Character: pos.Column - 1,
   332  	}
   333  }