go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/cli/printer/mql.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package printer
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"go.mondoo.com/cnquery/llx"
    14  	"go.mondoo.com/cnquery/types"
    15  	"go.mondoo.com/cnquery/utils/sortx"
    16  )
    17  
    18  // Results prints a full query with all data points
    19  // NOTE: ensure that results only contains results that match the bundle!
    20  func (print *Printer) Results(bundle *llx.CodeBundle, results map[string]*llx.RawResult) string {
    21  	assessment := llx.Results2Assessment(bundle, results)
    22  
    23  	if assessment != nil {
    24  		return print.Assessment(bundle, assessment)
    25  	}
    26  
    27  	var res strings.Builder
    28  	i := 0
    29  	for _, v := range results {
    30  		res.WriteString(print.Result(v, bundle))
    31  		if len(results) > 1 && len(results) != i+1 {
    32  			res.WriteString("\n")
    33  		}
    34  		i++
    35  	}
    36  	return res.String()
    37  }
    38  
    39  // Assessment prints a complete comparable assessment
    40  func (print *Printer) Assessment(bundle *llx.CodeBundle, assessment *llx.Assessment) string {
    41  	var res strings.Builder
    42  
    43  	var indent string
    44  	if len(assessment.Results) > 1 {
    45  		if assessment.Success {
    46  			res.WriteString(print.Secondary("[ok] "))
    47  		} else {
    48  			res.WriteString(print.Failed("[failed] "))
    49  		}
    50  		res.WriteString(bundle.Source)
    51  		res.WriteString("\n")
    52  		// once we displayed the overall status, lets indent the subs
    53  		indent = "  "
    54  	}
    55  
    56  	// indent all sub-results
    57  	for i := range assessment.Results {
    58  		cur := print.assessment(bundle, assessment.Results[i], indent)
    59  		res.WriteString(indent)
    60  		res.WriteString(cur)
    61  		res.WriteString("\n")
    62  	}
    63  
    64  	return res.String()
    65  }
    66  
    67  func isBooleanOp(op string) bool {
    68  	return op == "&&" || op == "||"
    69  }
    70  
    71  func (print *Printer) assessment(bundle *llx.CodeBundle, assessment *llx.AssessmentItem, indent string) string {
    72  	var codeID string
    73  	if bundle.CodeV2 != nil {
    74  		codeID = bundle.CodeV2.Id
    75  	} else {
    76  		return "error: no code id"
    77  	}
    78  
    79  	nextIndent := indent + "  "
    80  	if assessment.Error != "" {
    81  		var res strings.Builder
    82  		res.WriteString(print.Failed("[failed] "))
    83  		if bundle != nil && bundle.Labels != nil {
    84  			label := bundle.Labels.Labels[assessment.Checksum]
    85  			res.WriteString(label)
    86  		}
    87  		res.WriteString("\n")
    88  		res.WriteString(nextIndent)
    89  		res.WriteString("error: ")
    90  		res.WriteString(assessment.Error)
    91  		return res.String()
    92  	}
    93  
    94  	if assessment.Template != "" {
    95  		if assessment.Success {
    96  			return print.Secondary("[ok] true")
    97  		}
    98  
    99  		data := make([]string, len(assessment.Data))
   100  		for i := range assessment.Data {
   101  			data[i] = print.Primitive(assessment.Data[i], codeID, bundle, indent)
   102  		}
   103  		res := print.Failed("[failed] ") +
   104  			print.assessmentTemplate(assessment.Template, data)
   105  		return indentBlock(res, indent)
   106  	}
   107  
   108  	if isBooleanOp(assessment.Operation) {
   109  		if assessment.Success {
   110  			return print.Secondary("[ok] true")
   111  		}
   112  
   113  		var res strings.Builder
   114  		res.WriteString(print.Secondary("[failed] "))
   115  		res.WriteString(print.Primitive(assessment.Actual, codeID, bundle, nextIndent))
   116  		res.WriteString(" " + assessment.Operation + " ")
   117  		res.WriteString(print.Primitive(assessment.Expected, codeID, bundle, nextIndent))
   118  		return res.String()
   119  	}
   120  
   121  	if assessment.Success {
   122  		if assessment.Actual == nil {
   123  			return print.Secondary("[ok] ") + " (missing result)"
   124  		}
   125  
   126  		return print.Secondary("[ok]") +
   127  			" value: " +
   128  			print.Primitive(assessment.Actual, codeID, bundle, indent)
   129  	}
   130  
   131  	var res strings.Builder
   132  	res.WriteString(print.Failed("[failed] "))
   133  	if bundle != nil && bundle.Labels != nil {
   134  		label, ok := bundle.Labels.Labels[assessment.Checksum]
   135  		if ok {
   136  			res.WriteString(label)
   137  		}
   138  	}
   139  	res.WriteString("\n")
   140  
   141  	if assessment.Expected != nil {
   142  		res.WriteString(nextIndent)
   143  		res.WriteString("expected: " + assessment.Operation + " ")
   144  		res.WriteString(print.Primitive(assessment.Expected, codeID, bundle, nextIndent))
   145  		res.WriteString("\n")
   146  	}
   147  
   148  	if assessment.Actual != nil {
   149  		res.WriteString(nextIndent)
   150  		res.WriteString("actual:   ")
   151  		res.WriteString(print.Primitive(assessment.Actual, codeID, bundle, nextIndent))
   152  	}
   153  
   154  	return res.String()
   155  }
   156  
   157  // Result prints the llx raw result into a string
   158  func (print *Printer) Result(result *llx.RawResult, bundle *llx.CodeBundle) string {
   159  	if result == nil {
   160  		return "< no result? >"
   161  	}
   162  
   163  	return print.DataWithLabel(result.Data, result.CodeID, bundle, "")
   164  }
   165  
   166  func (print *Printer) label(ref string, bundle *llx.CodeBundle, isResource bool) string {
   167  	if bundle == nil {
   168  		return ""
   169  	}
   170  
   171  	labels := bundle.Labels
   172  	if labels == nil {
   173  		return fmt.Sprintf("["+print.Primary("%s")+"] ", ref)
   174  	}
   175  
   176  	label := labels.Labels[ref]
   177  	if label == "" {
   178  		return ""
   179  	}
   180  
   181  	return print.Primary(label) + ": "
   182  }
   183  
   184  func (print *Printer) array(typ types.Type, data []interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   185  	if len(data) == 0 {
   186  		return "[]"
   187  	}
   188  
   189  	var res strings.Builder
   190  	res.WriteString("[\n")
   191  
   192  	code := bundle.CodeV2
   193  	ep := code.Blocks[0].Entrypoints[0]
   194  	chunk := code.Chunk(ep)
   195  	listType := ""
   196  	switch chunk.Id {
   197  	case "$one", "$all", "$none", "$any":
   198  		ref := chunk.Function.Binding
   199  		listChunk := code.Chunk(ref)
   200  		listType = types.Type(listChunk.Type()).Child().Label()
   201  		listType += " "
   202  	}
   203  
   204  	for i := range data {
   205  		res.WriteString(fmt.Sprintf(
   206  			indent+"  %d: %s%s\n",
   207  			i,
   208  			listType,
   209  			print.Data(typ.Child(), data[i], codeID, bundle, indent+"  "),
   210  		))
   211  	}
   212  
   213  	res.WriteString(indent + "]")
   214  	return res.String()
   215  }
   216  
   217  func (print *Printer) assessmentTemplate(template string, data []string) string {
   218  	res := template
   219  	for i := range data {
   220  		r := "$" + strconv.Itoa(i)
   221  		res = strings.ReplaceAll(res, r, data[i])
   222  	}
   223  	return res
   224  }
   225  
   226  func (print *Printer) dataAssessment(codeID string, data map[string]interface{}, bundle *llx.CodeBundle) string {
   227  	var assertion *llx.AssertionMessage
   228  	var ok bool
   229  
   230  	assertion, ok = bundle.Assertions[codeID]
   231  	if !ok {
   232  		return ""
   233  	}
   234  
   235  	checksums := assertion.Checksums
   236  	if assertion.DecodeBlock {
   237  		if len(assertion.Checksums) < 2 {
   238  			return ""
   239  		}
   240  
   241  		v, ok := data[assertion.Checksums[0]]
   242  		if !ok {
   243  			return ""
   244  		}
   245  		raw, ok := v.(*llx.RawData)
   246  		if !ok {
   247  			return ""
   248  		}
   249  		data, ok = raw.Value.(map[string]interface{})
   250  		if !ok {
   251  			return ""
   252  		}
   253  		checksums = checksums[1:]
   254  	}
   255  
   256  	fields := make([]string, len(checksums))
   257  	for i := range checksums {
   258  		v, ok := data[checksums[i]]
   259  		if !ok {
   260  			return ""
   261  		}
   262  
   263  		val := v.(*llx.RawData)
   264  		fields[i] = print.Data(val.Type, val.Value, "", bundle, "")
   265  	}
   266  
   267  	return print.assessmentTemplate(assertion.Template, fields)
   268  }
   269  
   270  func (print *Printer) refMap(typ types.Type, data map[string]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   271  	if len(data) == 0 {
   272  		return "{}"
   273  	}
   274  
   275  	var res strings.Builder
   276  	res.WriteString("{\n")
   277  
   278  	// we need to separate entries that are unlabelled (eg part of an assertion)
   279  	labeledKeys := []string{}
   280  	keys := sortx.Keys(data)
   281  	for i := range keys {
   282  		if _, ok := bundle.Labels.Labels[keys[i]]; ok {
   283  			labeledKeys = append(labeledKeys, keys[i])
   284  		}
   285  	}
   286  
   287  	for _, k := range labeledKeys {
   288  		if k == "_" {
   289  			continue
   290  		}
   291  
   292  		v := data[k]
   293  		label := print.label(k, bundle, true)
   294  		val := v.(*llx.RawData)
   295  
   296  		if val.Error != nil {
   297  			res.WriteString(indent + "  " + label + print.Error(val.Error.Error()) + "\n")
   298  			continue
   299  		}
   300  
   301  		if truthy, _ := val.IsTruthy(); !truthy {
   302  			assertion := print.dataAssessment(k, data, bundle)
   303  			if assertion != "" {
   304  				assertion = print.Failed("[failed]") + " " + strings.Trim(assertion, "\n\t ")
   305  				assertion = indentBlock(assertion, indent+"  ")
   306  				res.WriteString(indent + "  " + assertion + "\n")
   307  				continue
   308  			}
   309  		}
   310  
   311  		data := print.Data(val.Type, val.Value, k, bundle, indent+"  ")
   312  		res.WriteString(indent + "  " + label + data + "\n")
   313  	}
   314  
   315  	res.WriteString(indent + "}")
   316  	return res.String()
   317  }
   318  
   319  func (print *Printer) stringMap(typ types.Type, data map[string]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   320  	if len(data) == 0 {
   321  		return "{}"
   322  	}
   323  
   324  	var res strings.Builder
   325  	res.WriteString("{\n")
   326  
   327  	keys := sortx.Keys(data)
   328  	for _, k := range keys {
   329  		v := data[k]
   330  		val := print.Data(typ.Child(), v, k, bundle, indent+"  ")
   331  		res.WriteString(fmt.Sprintf(indent+"  %s: %s\n", k, val))
   332  	}
   333  
   334  	res.WriteString(indent + "}")
   335  	return res.String()
   336  }
   337  
   338  func (print *Printer) dict(typ types.Type, raw interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   339  	if raw == nil {
   340  		return print.Secondary("null")
   341  	}
   342  
   343  	switch data := raw.(type) {
   344  	case bool:
   345  		if data {
   346  			return print.Secondary("true")
   347  		}
   348  		return print.Secondary("false")
   349  
   350  	case int64:
   351  		return print.Secondary(fmt.Sprintf("%d", data))
   352  
   353  	case float64:
   354  		return print.Secondary(fmt.Sprintf("%f", data))
   355  
   356  	case string:
   357  		return print.Secondary(llx.PrettyPrintString(data))
   358  
   359  	case time.Time:
   360  		return print.Secondary(data.String())
   361  
   362  	case []interface{}:
   363  		if len(data) == 0 {
   364  			return "[]"
   365  		}
   366  
   367  		var res strings.Builder
   368  		res.WriteString("[\n")
   369  
   370  		for i := range data {
   371  			res.WriteString(fmt.Sprintf(
   372  				indent+"  %d: %s\n",
   373  				i,
   374  				print.dict(typ, data[i], "", bundle, indent+"  "),
   375  			))
   376  		}
   377  
   378  		res.WriteString(indent + "]")
   379  		return res.String()
   380  
   381  	case map[string]interface{}:
   382  		if len(data) == 0 {
   383  			return "{}"
   384  		}
   385  
   386  		var res strings.Builder
   387  		res.WriteString("{\n")
   388  
   389  		keys := sortx.Keys(data)
   390  		for _, k := range keys {
   391  			s := print.dict(typ, data[k], "", bundle, indent+"  ")
   392  			res.WriteString(fmt.Sprintf(indent+"  %s: %s\n", k, s))
   393  		}
   394  
   395  		res.WriteString(indent + "}")
   396  		return res.String()
   397  
   398  	default:
   399  		return print.Secondary(fmt.Sprintf("%+v", raw))
   400  	}
   401  }
   402  
   403  func (print *Printer) intMap(typ types.Type, data map[int]interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   404  	var res strings.Builder
   405  	res.WriteString("{\n")
   406  
   407  	for i := range data {
   408  		value := data[i]
   409  		res.WriteString(fmt.Sprintf(indent+"  %d: "+print.Secondary("%#v")+"\n", i, value))
   410  	}
   411  
   412  	res.WriteString(indent + "}")
   413  	return res.String()
   414  }
   415  
   416  var codeBlockIds = map[string]struct{}{
   417  	"{}": {},
   418  	"if": {},
   419  }
   420  
   421  func isCodeBlock(codeID string, bundle *llx.CodeBundle) bool {
   422  	if bundle == nil {
   423  		return false
   424  	}
   425  
   426  	_, ok := bundle.Labels.Labels[codeID]
   427  	return ok
   428  }
   429  
   430  func (print *Printer) autoExpand(blockRef uint64, data interface{}, bundle *llx.CodeBundle, indent string) string {
   431  	var res strings.Builder
   432  
   433  	if arr, ok := data.([]interface{}); ok {
   434  		if len(arr) == 0 {
   435  			return "[]"
   436  		}
   437  
   438  		prefix := indent + "  "
   439  		res.WriteString("[\n")
   440  		for i := range arr {
   441  			c := print.autoExpand(blockRef, arr[i], bundle, prefix)
   442  			res.WriteString(prefix)
   443  			res.WriteString(strconv.Itoa(i))
   444  			res.WriteString(": ")
   445  			res.WriteString(c)
   446  			res.WriteByte('\n')
   447  		}
   448  		res.WriteString(indent + "]")
   449  		return res.String()
   450  	}
   451  
   452  	if data == nil {
   453  		return print.Secondary("null")
   454  	}
   455  
   456  	m, ok := data.(map[string]interface{})
   457  	if !ok {
   458  		return "data is not a map to auto-expand"
   459  	}
   460  
   461  	block := bundle.CodeV2.Block(blockRef)
   462  	var name string
   463  
   464  	self, ok := m["_"].(*llx.RawData)
   465  	if ok {
   466  		name = self.Type.ResourceName()
   467  	} else if block != nil {
   468  		// We end up here when we deal with array resources. In that case,
   469  		// the block's first element is typed as an individual resource, so we can
   470  		// use that.
   471  		typ := block.Chunks[0].Type()
   472  		name = typ.ResourceName()
   473  	}
   474  	if name == "" {
   475  		name = "<unknown>"
   476  	}
   477  
   478  	res.WriteString(name)
   479  
   480  	if block != nil {
   481  		// important to process them in this order
   482  		for _, ref := range block.Entrypoints {
   483  			checksum := bundle.CodeV2.Checksums[ref]
   484  			v, ok := m[checksum]
   485  			if !ok {
   486  				continue
   487  			}
   488  			vv, ok := v.(*llx.RawData)
   489  			if !ok {
   490  				continue
   491  			}
   492  
   493  			label := bundle.Labels.Labels[checksum]
   494  			val := print.Data(vv.Type, vv.Value, checksum, bundle, indent)
   495  			res.WriteByte(' ')
   496  			res.WriteString(label)
   497  			res.WriteByte('=')
   498  			res.WriteString(val)
   499  		}
   500  	}
   501  
   502  	return res.String()
   503  }
   504  
   505  func (print *Printer) Data(typ types.Type, data interface{}, codeID string, bundle *llx.CodeBundle, indent string) string {
   506  	if typ.NotSet() {
   507  		return "no data available"
   508  	}
   509  
   510  	if blockRef, ok := bundle.AutoExpand[codeID]; ok {
   511  		return print.autoExpand(blockRef, data, bundle, indent)
   512  	}
   513  
   514  	switch typ.Underlying() {
   515  	case types.Any:
   516  		return indent + fmt.Sprintf("any: %#v", data)
   517  	case types.Ref:
   518  		if d, ok := data.(uint64); ok {
   519  			return "ref" + print.Primary(fmt.Sprintf("<%d,%d>", d>>32, d&0xFFFFFFFF))
   520  		} else {
   521  			return "ref" + print.Primary(fmt.Sprintf("%d", data.(int32)))
   522  		}
   523  	case types.Nil:
   524  		return print.Secondary("null")
   525  	case types.Bool:
   526  		if data == nil {
   527  			return print.Secondary("null")
   528  		}
   529  		if data.(bool) {
   530  			return print.Secondary("true")
   531  		}
   532  		return print.Secondary("false")
   533  	case types.Int:
   534  		if data == nil {
   535  			return print.Secondary("null")
   536  		}
   537  		if data.(int64) == math.MaxInt64 {
   538  			return print.Secondary("Infinity")
   539  		}
   540  		if data.(int64) == math.MinInt64 {
   541  			return print.Secondary("-Infinity")
   542  		}
   543  		return print.Secondary(strconv.FormatInt(data.(int64), 10))
   544  	case types.Float:
   545  		if data == nil {
   546  			return print.Secondary("null")
   547  		}
   548  		if math.IsInf(data.(float64), 1) {
   549  			return print.Secondary("Infinity")
   550  		}
   551  		if math.IsInf(data.(float64), -1) {
   552  			return print.Secondary("-Infinity")
   553  		}
   554  		return print.Secondary(strconv.FormatFloat(data.(float64), 'g', -1, 64))
   555  	case types.String:
   556  		if data == nil {
   557  			return print.Secondary("null")
   558  		}
   559  		return print.Secondary(llx.PrettyPrintString(data.(string)))
   560  	case types.Regex:
   561  		if data == nil {
   562  			return print.Secondary("null")
   563  		}
   564  		return print.Secondary(fmt.Sprintf("/%s/", data))
   565  	case types.Time:
   566  		if data == nil {
   567  			return print.Secondary("null")
   568  		}
   569  		time := data.(*time.Time)
   570  		if time == nil {
   571  			return print.Secondary("null")
   572  		}
   573  
   574  		if *time == llx.NeverPastTime || *time == llx.NeverFutureTime {
   575  			return print.Secondary("Never")
   576  		}
   577  
   578  		if time.Unix() > 0 {
   579  			return print.Secondary(time.String())
   580  		}
   581  
   582  		seconds := llx.TimeToDuration(time)
   583  		minutes := seconds / 60
   584  		hours := minutes / 60
   585  		days := hours / 24
   586  
   587  		var res strings.Builder
   588  		if days > 0 {
   589  			res.WriteString(fmt.Sprintf("%d days ", days))
   590  		}
   591  		if hours%24 != 0 {
   592  			res.WriteString(fmt.Sprintf("%d hours ", hours%24))
   593  		}
   594  		if minutes%24 != 0 {
   595  			res.WriteString(fmt.Sprintf("%d minutes ", minutes%60))
   596  		}
   597  		// if we haven't printed any of the other pieces (days/hours/minutes) then print this
   598  		// if we have, then check if this is non-zero
   599  		if minutes == 0 || seconds%60 != 0 {
   600  			res.WriteString(fmt.Sprintf("%d seconds", seconds%60))
   601  		}
   602  
   603  		return print.Secondary(res.String())
   604  	case types.Dict:
   605  		return print.dict(typ, data, codeID, bundle, indent)
   606  
   607  	case types.Score:
   608  		return llx.ScoreString(data.([]byte))
   609  
   610  	case types.Block:
   611  		return print.refMap(typ, data.(map[string]interface{}), codeID, bundle, indent)
   612  
   613  	case types.ArrayLike:
   614  		if data == nil {
   615  			return print.Secondary("null")
   616  		}
   617  		return print.array(typ, data.([]interface{}), codeID, bundle, indent)
   618  
   619  	case types.MapLike:
   620  		if data == nil {
   621  			return print.Secondary("null")
   622  		}
   623  		if typ.Key() == types.String {
   624  			return print.stringMap(typ, data.(map[string]interface{}), codeID, bundle, indent)
   625  		}
   626  		if typ.Key() == types.Int {
   627  			return print.intMap(typ, data.(map[int]interface{}), codeID, bundle, indent)
   628  		}
   629  		return "unable to render map, its type is not supported: " + typ.Label() + ", raw: " + fmt.Sprintf("%#v", data)
   630  
   631  	case types.ResourceLike:
   632  		if data == nil {
   633  			return print.Secondary("null")
   634  		}
   635  
   636  		r := data.(llx.Resource)
   637  		idline := r.MqlName()
   638  		if id := r.MqlID(); id != "" {
   639  			idline += " id = " + id
   640  		}
   641  
   642  		return idline
   643  	case types.FunctionLike:
   644  		if d, ok := data.(uint64); ok {
   645  			return indent + fmt.Sprintf("=> <%d,0>", d>>32)
   646  		} else {
   647  			return indent + fmt.Sprintf("=> %#v", data)
   648  		}
   649  	default:
   650  		return indent + fmt.Sprintf("🤷 %#v", data)
   651  	}
   652  }
   653  
   654  // DataWithLabel prints RawData into a string
   655  func (print *Printer) DataWithLabel(r *llx.RawData, codeID string, bundle *llx.CodeBundle, indent string) string {
   656  	b := strings.Builder{}
   657  	if r.Error != nil {
   658  		b.WriteString(print.Error(strings.TrimSpace(r.Error.Error())))
   659  		b.WriteByte('\n')
   660  	}
   661  
   662  	b.WriteString(print.label(codeID, bundle, r.Type.IsResource()))
   663  	b.WriteString(print.Data(r.Type, r.Value, codeID, bundle, indent))
   664  	return b.String()
   665  }
   666  
   667  // CodeBundle prints a bundle to a string
   668  func (print *Printer) CodeBundle(bundle *llx.CodeBundle) string {
   669  	var res strings.Builder
   670  
   671  	res.WriteString(print.CodeV2(bundle.CodeV2, bundle, ""))
   672  
   673  	for idx := range bundle.Suggestions {
   674  		info := bundle.Suggestions[idx]
   675  		res.WriteString(print.Yellow("- suggestion: "+info.Field) + "\n")
   676  	}
   677  
   678  	return res.String()
   679  }
   680  
   681  func (print *Printer) CodeV2(code *llx.CodeV2, bundle *llx.CodeBundle, indent string) string {
   682  	var res strings.Builder
   683  	indent += "   "
   684  
   685  	for i := range code.Blocks {
   686  		block := code.Blocks[i]
   687  
   688  		res.WriteString(print.Secondary(fmt.Sprintf("-> block %d\n", i+1)))
   689  
   690  		res.WriteString(indent)
   691  		res.WriteString("entrypoints: [")
   692  		for idx, ep := range block.Entrypoints {
   693  			res.WriteString(fmt.Sprintf("<%d,%d>", ep>>32, ep&0xFFFFFFFF))
   694  			if idx != len(block.Entrypoints)-1 {
   695  				res.WriteString(" ")
   696  			}
   697  		}
   698  		res.WriteString("]\n")
   699  
   700  		for j := range block.Chunks {
   701  			res.WriteString("   ")
   702  			print.chunkV2(j, block.Chunks[j], block, bundle, indent, &res)
   703  		}
   704  	}
   705  
   706  	return res.String()
   707  }
   708  
   709  func (print *Printer) chunkV2(idx int, chunk *llx.Chunk, block *llx.Block, bundle *llx.CodeBundle, indent string, w *strings.Builder) {
   710  	sidx := strconv.Itoa(idx+1) + ": "
   711  
   712  	switch chunk.Call {
   713  	case llx.Chunk_FUNCTION:
   714  		w.WriteString(print.Primary(sidx))
   715  	case llx.Chunk_PRIMITIVE:
   716  		w.WriteString(print.Secondary(sidx))
   717  	default:
   718  		w.WriteString(print.Error(sidx))
   719  	}
   720  
   721  	if chunk.Id != "" {
   722  		w.WriteString(chunk.Id + " ")
   723  	}
   724  
   725  	if chunk.Primitive != nil {
   726  		primitive := chunk.Primitive
   727  
   728  		// special case: function arguments are supplied by the caller, so we fill
   729  		// in the context for all resource references since they cannot have default values
   730  		if idx < int(block.Parameters) && len(primitive.Value) == 0 && types.Type(primitive.Type).IsResource() {
   731  			primitive = &llx.Primitive{Type: primitive.Type, Value: []byte("context")}
   732  		}
   733  
   734  		// FIXME: this is definitely the wrong ID
   735  		w.WriteString(print.Primitive(primitive, bundle.CodeV2.Id, bundle, indent))
   736  	}
   737  
   738  	if chunk.Function != nil {
   739  		w.WriteString(print.functionV2(chunk.Function, bundle.CodeV2.Id, bundle, indent))
   740  	}
   741  
   742  	w.WriteString("\n")
   743  }
   744  
   745  func (print *Printer) functionV2(f *llx.Function, codeID string, bundle *llx.CodeBundle, indent string) string {
   746  	argsStr := ""
   747  	if len(f.Args) > 0 {
   748  		argsStr = " (" + strings.TrimSpace(print.primitives(f.Args, codeID, bundle, indent)) + ")"
   749  	}
   750  
   751  	return "bind: <" + strconv.Itoa(int(f.Binding>>32)) + "," +
   752  		strconv.Itoa(int(f.Binding&0xFFFFFFFF)) +
   753  		"> type:" + types.Type(f.Type).Label() +
   754  		argsStr
   755  }
   756  
   757  func (print *Printer) primitives(list []*llx.Primitive, codeID string, bundle *llx.CodeBundle, indent string) string {
   758  	if len(list) == 0 {
   759  		return ""
   760  	}
   761  	var res strings.Builder
   762  
   763  	if len(list) >= 1 {
   764  		res.WriteString(print.Primitive(list[0], codeID, bundle, indent))
   765  	}
   766  
   767  	for i := 1; i < len(list); i++ {
   768  		res.WriteString(", ")
   769  		res.WriteString(strings.TrimLeft(print.Primitive(list[i], codeID, bundle, indent), " "))
   770  	}
   771  	return res.String()
   772  }
   773  
   774  // Primitive prints the llx primitive to a string
   775  func (print *Printer) Primitive(primitive *llx.Primitive, codeID string, bundle *llx.CodeBundle, indent string) string {
   776  	if primitive == nil {
   777  		return "?"
   778  	}
   779  
   780  	if primitive.Value == nil && primitive.Array == nil && primitive.Map == nil {
   781  		return "_"
   782  	}
   783  	raw := primitive.RawData()
   784  	return print.Data(raw.Type, raw.Value, codeID, bundle, indent)
   785  }
   786  
   787  func indentBlock(text string, indent string) string {
   788  	return strings.ReplaceAll(text, "\n", "\n"+indent)
   789  }