[BACK]Return to mkvarusechecker.go CVS log [TXT][DIR] Up to [cvs.NetBSD.org] / pkgsrc / pkgtools / pkglint / files

File: [cvs.NetBSD.org] / pkgsrc / pkgtools / pkglint / files / mkvarusechecker.go (download)

Revision 1.3, Mon Dec 9 20:38:16 2019 UTC (3 months, 2 weeks ago) by rillig
Branch: MAIN
CVS Tags: pkgsrc-2019Q4-base, pkgsrc-2019Q4
Changes since 1.2: +7 -9 lines

pkgtools/pkglint: update to 19.3.16

Changes since 19.3.15:

When a package-settable variable gets a default value using the ?=
operator, pkglint no longer suggests to include bsd.prefs.mk, since that
doesn't make sense. Including bsd.prefs.mk only defines user-settable
and system-provided variables.

User and group names may be a single character only. While not widely
used, it's syntactically valid and there's no reason to prevent this.

In variable assignments, when pkglint removes unnecessary whitespace
between the variable name and the operator, it keeps the indentation of
the variable value the same as before. Previously, the indentation had
been changed, which required another run of pkglint --autofix.

PREFIX can only be used as a replacement for LOCALBASE after the whole
package Makefile has been loaded. This is because PREFIX is defined
very late, by bsd.pkg.mk. Therefore, don't suggest to replace LOCALBASE
with PREFIX in .if conditions.

When pkglint suggests to replace INSTALL_DATA_DIR commands with setting
INSTALLATION_DIRS instead, paths with a trailing slash are correctly
looked up in the PLIST. This suggests to use AUTO_MKDIRS more often.

package pkglint

import "strings"

type MkVarUseChecker struct {
	use     *MkVarUse
	vartype *Vartype

	MkLines *MkLines
	MkLine  *MkLine
}

func NewMkVarUseChecker(use *MkVarUse, mklines *MkLines, mkline *MkLine) *MkVarUseChecker {
	vartype := G.Pkgsrc.VariableType(mklines, use.varname)

	return &MkVarUseChecker{use, vartype, mklines, mkline}
}

// CheckVaruse checks a single use of a variable in a specific context.
func (ck *MkVarUseChecker) Check(vuc *VarUseContext) {
	if ck.use.IsExpression() {
		return
	}

	ck.checkUndefined()
	ck.checkPermissions(vuc)

	ck.checkVarname(vuc.time)
	ck.checkModifiers()
	ck.checkQuoting(vuc)

	ck.checkBuildDefs()
	ck.checkDeprecated()

	NewMkLineChecker(ck.MkLines, ck.MkLine).
		checkTextVarUse(ck.use.varname, ck.vartype, vuc.time)
}

func (ck *MkVarUseChecker) checkUndefined() {
	varuse := ck.use
	vartype := ck.vartype
	varname := varuse.varname

	switch {
	case !G.Opts.WarnExtra,
		// Well-known variables are probably defined by the infrastructure.
		vartype != nil && !vartype.IsGuessed(),
		// TODO: At load time, check ck.MkLines.loadVars instead of allVars.
		ck.MkLines.allVars.IsDefinedSimilar(varname),
		ck.MkLines.forVars[varname],
		ck.MkLines.allVars.Mentioned(varname) != nil,
		G.Pkg != nil && G.Pkg.vars.IsDefinedSimilar(varname),
		containsVarRef(varname),
		G.Pkgsrc.vartypes.IsDefinedCanon(varname),
		varname == "":
		return
	}

	if ck.MkLines.once.FirstTimeSlice("used but not defined", varname) {
		ck.MkLine.Warnf("%s is used but not defined.", varname)
	}
}

func (ck *MkVarUseChecker) checkModifiers() {
	if len(ck.use.modifiers) == 0 {
		return
	}

	ck.checkModifiersSuffix()
	ck.checkModifiersRange()

	// TODO: Add checks for a single modifier, among them:
	// TODO: Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,}.
	// TODO: Suggest to replace ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.
	// TODO: Investigate why :Q is not checked at this exact place.
}

func (ck *MkVarUseChecker) checkModifiersSuffix() {
	varuse := ck.use
	vartype := ck.vartype

	if !varuse.modifiers[0].IsSuffixSubst() || vartype == nil || vartype.IsList() {
		return
	}

	ck.MkLine.Warnf("The :from=to modifier should only be used with lists, not with %s.", varuse.varname)
	ck.MkLine.Explain(
		"Instead of (for example):",
		"\tMASTER_SITES=\t${HOMEPAGE:=repository/}",
		"",
		"Write:",
		"\tMASTER_SITES=\t${HOMEPAGE}repository/",
		"",
		"This expresses the intention of the code more clearly.")
}

// checkModifiersRange suggests to replace
// ${VAR:S,^,__magic__,1:M__magic__*:S,^__magic__,,} with the simpler ${VAR:[1]}.
func (ck *MkVarUseChecker) checkModifiersRange() {
	varuse := ck.use
	mods := varuse.modifiers

	if len(mods) != 3 {
		return
	}

	m, _, from, to, options := mods[0].MatchSubst()
	if !m || from != "^" || !matches(to, `^\w+$`) || options != "1" {
		return
	}

	magic := to
	m, positive, pattern, _ := mods[1].MatchMatch()
	if !m || !positive || pattern != magic+"*" {
		return
	}

	m, _, from, to, options = mods[2].MatchSubst()
	if !m || from != "^"+magic || to != "" || options != "" {
		return
	}

	fix := ck.MkLine.Autofix()
	fix.Notef("The modifier %q can be written as %q.", varuse.Mod(), ":[1]")
	fix.Explain(
		"The range modifier is much easier to understand than the",
		"complicated regular expressions, which were needed before",
		"the year 2006.")
	fix.Replace(varuse.Mod(), ":[1]")
	fix.Apply()
}

func (ck *MkVarUseChecker) checkVarname(time VucTime) {
	varname := ck.use.varname
	if varname == "@" {
		ck.MkLine.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
		ck.MkLine.Explain(
			"It is more readable and prevents confusion with the shell variable",
			"of the same name.")
	}

	if varname == "LOCALBASE" && !G.Infrastructure && time == VucRunTime {
		fix := ck.MkLine.Autofix()
		fix.Warnf("Please use PREFIX instead of LOCALBASE.")
		fix.ReplaceRegex(`\$\{LOCALBASE\b`, "${PREFIX", 1)
		fix.Apply()
	}
}

// checkPermissions checks the permissions when a variable is used,
// be it in a variable assignment, in a shell command, a conditional, or
// somewhere else.
//
// See checkVarassignLeftPermissions.
func (ck *MkVarUseChecker) checkPermissions(vuc *VarUseContext) {
	if !G.Opts.WarnPerm {
		return
	}
	if G.Infrastructure {
		// As long as vardefs.go doesn't explicitly define permissions for
		// infrastructure files, skip the check completely. This avoids
		// many wrong warnings.
		return
	}

	if trace.Tracing {
		defer trace.Call(vuc)()
	}

	// This is the type of the variable that is being used. Not to
	// be confused with vuc.vartype, which is the type of the
	// context in which the variable is used (often a ShellCommand
	// or, in an assignment, the type of the left hand side variable).
	varname := ck.use.varname
	vartype := ck.vartype
	if vartype == nil {
		if trace.Tracing {
			trace.Step1("No type definition found for %q.", varname)
		}
		return
	}

	if vartype.IsGuessed() {
		return
	}

	// Do not warn about unknown infrastructure variables.
	// These have all permissions to prevent warnings when they are used.
	// But when other variables are assigned to them it would seem as if
	// these other variables could become evaluated at load time.
	// And this is something that most variables do not allow.
	if vuc.vartype != nil && vuc.vartype.basicType == BtUnknown {
		return
	}

	basename := ck.MkLine.Basename
	if basename == "hacks.mk" {
		return
	}

	effPerms := vartype.EffectivePermissions(basename)
	if effPerms.Contains(aclpUseLoadtime) {
		ck.checkUseAtLoadTime(vuc.time)

		// Since the variable may be used at load time, it probably
		// may be used at run time as well. If it weren't, that would
		// be a rather strange permissions set.
		return
	}

	// At this point the variable must not be used at load time.
	// Now determine whether it is directly used at load time because
	// the context already says so or, a little trickier, if it might
	// be used at load time somewhere in the future because it is
	// assigned to another variable, and that variable is allowed
	// to be used at load time.
	directly := vuc.time == VucLoadTime
	indirectly := !directly && vuc.vartype != nil &&
		vuc.vartype.Union().Contains(aclpUseLoadtime)

	if !directly && !indirectly && effPerms.Contains(aclpUse) {
		// At this point the variable is either used at run time, or the
		// time is not known.
		return
	}

	if directly || indirectly {
		// At this point the variable is used at load time although that
		// is not allowed by the permissions. The variable could be a tool
		// variable, and these tool variables have special rules.
		tool := G.ToolByVarname(ck.MkLines, varname)
		if tool != nil {

			// Whether a tool variable may be used at load time depends on
			// whether bsd.prefs.mk has been included before. That file
			// examines the tools that have been added to USE_TOOLS up to
			// this point and makes their variables available for use at
			// load time.
			if !tool.UsableAtLoadTime(ck.MkLines.Tools.SeenPrefs) {
				ck.warnToolLoadTime(varname, tool)
			}
			return
		}
	}

	if ck.MkLines.once.FirstTimeSlice("checkPermissions", varname) {
		ck.warnPermissions(vuc.vartype, varname, vartype, directly, indirectly)
	}
}

func (ck *MkVarUseChecker) warnPermissions(
	vucVartype *Vartype, varname string, vartype *Vartype, directly, indirectly bool) {

	mkline := ck.MkLine

	anyPerms := vartype.Union()
	if !anyPerms.Contains(aclpUse) && !anyPerms.Contains(aclpUseLoadtime) {
		mkline.Warnf("%s should not be used in any file; it is a write-only variable.", varname)
		ck.explainPermissions(varname, vartype)
		return
	}

	if indirectly {
		// Some of the guessed variables may be used at load time. But since the
		// variable type and these permissions are guessed, pkglint should not
		// issue the following warning, since it is often wrong.
		if vucVartype.IsGuessed() {
			return
		}

		mkline.Warnf("%s should not be used indirectly at load time (via %s).",
			varname, mkline.Varname())
		ck.explainPermissions(varname, vartype,
			"The variable on the left-hand side may be evaluated at load time,",
			"but the variable on the right-hand side should not.",
			"Because of the assignment in this line, the variable might be",
			"used indirectly at load time, before it is guaranteed to be",
			"properly initialized.")
		return
	}

	needed := aclpUse
	if directly {
		needed = aclpUseLoadtime
	}
	alternativeFiles := vartype.AlternativeFiles(needed)

	loadTimeExplanation := func() []string {
		return []string{
			"Many variables, especially lists of something, get their values incrementally.",
			"Therefore it is generally unsafe to rely on their",
			"value until it is clear that it will never change again.",
			"This point is reached when the whole package Makefile is loaded and",
			"execution of the shell commands starts; in some cases earlier.",
			"",
			"Additionally, when using the \":=\" operator, each $$ is replaced",
			"with a single $, so variables that have references to shell",
			"variables or regular expressions are modified in a subtle way."}
	}

	switch {
	case alternativeFiles == "" && directly:
		mkline.Warnf("%s should not be used at load time in any file.", varname)
		ck.explainPermissions(varname, vartype, loadTimeExplanation()...)

	case alternativeFiles == "":
		mkline.Warnf("%s should not be used in any file.", varname)
		ck.explainPermissions(varname, vartype, loadTimeExplanation()...)

	case directly:
		mkline.Warnf(
			"%s should not be used at load time in this file; "+
				"it would be ok in %s.",
			varname, alternativeFiles)
		ck.explainPermissions(varname, vartype, loadTimeExplanation()...)

	default:
		mkline.Warnf(
			"%s should not be used in this file; it would be ok in %s.",
			varname, alternativeFiles)
		ck.explainPermissions(varname, vartype)
	}
}

func (ck *MkVarUseChecker) explainPermissions(varname string, vartype *Vartype, intro ...string) {
	if !G.Logger.Opts.Explain {
		return
	}

	// TODO: Starting with the second explanation, omit the common part. Instead, only list the permission rules.

	var expl []string

	if len(intro) > 0 {
		expl = append(expl, intro...)
		expl = append(expl, "")
	}

	expl = append(expl,
		"The allowed actions for a variable are determined based on the file",
		"name in which the variable is used or defined.",
		sprintf("The rules for %s are:", varname),
		"")

	for _, rule := range vartype.aclEntries {
		perms := rule.permissions.HumanString()

		files := rule.matcher.originalPattern
		if files == "*" {
			files = "any file"
		}

		if perms != "" {
			expl = append(expl, sprintf("* in %s, it may be %s", files, perms))
		} else {
			expl = append(expl, sprintf("* in %s, it should not be accessed at all", files))
		}
	}

	expl = append(expl,
		"",
		"If these rules seem to be incorrect, please ask on the tech-pkg@NetBSD.org mailing list.")

	ck.MkLine.Explain(expl...)
}

func (ck *MkVarUseChecker) checkUseAtLoadTime(time VucTime) {
	if time != VucLoadTime {
		return
	}
	if ck.vartype.IsAlwaysInScope() || ck.MkLines.Tools.SeenPrefs {
		return
	}
	if G.Pkg != nil && G.Pkg.seenPrefs {
		return
	}
	mkline := ck.MkLine
	basename := mkline.Basename
	if basename == "builtin.mk" {
		return
	}

	if ck.vartype.IsPackageSettable() {
		// For package-settable variables, the explanation below
		// doesn't make sense since including bsd.prefs.mk won't
		// define any package-settable variables.
		return
	}

	if !ck.MkLines.once.FirstTime("bsd.prefs.mk") {
		return
	}

	include := condStr(
		basename == "buildlink3.mk",
		"mk/bsd.fast.prefs.mk",
		"mk/bsd.prefs.mk")
	currInclude := G.Pkgsrc.File(NewPkgsrcPath(NewPath(include)))

	mkline.Warnf("To use %s at load time, .include %q first.",
		ck.use.varname, mkline.Rel(currInclude))
	mkline.Explain(
		"The user-settable variables and several other variables",
		"from the pkgsrc infrastructure are only available",
		"after the preferences have been loaded.",
		"",
		"Before that, these variables are undefined.")
}

// warnToolLoadTime logs a warning that the tool ${varname}
// should not be used at load time.
func (ck *MkVarUseChecker) warnToolLoadTime(varname string, tool *Tool) {
	// TODO: While using a tool by its variable name may be ok at load time,
	//  doing the same with the plain name of a tool is never ok.
	//  "VAR!= cat" is never guaranteed to call the correct cat.
	//  Even for shell builtins like echo and printf, bmake may decide
	//  to skip the shell and execute the commands via execve, which
	//  means that even echo is not a shell-builtin anymore.

	// TODO: Replace "parse time" with "load time" everywhere.

	if tool.Validity == AfterPrefsMk {
		ck.MkLine.Warnf("To use the tool ${%s} at load time, bsd.prefs.mk has to be included before.", varname)
		return
	}

	if ck.MkLine.Basename == "Makefile" {
		pkgsrcTool := G.Pkgsrc.Tools.ByName(tool.Name)
		if pkgsrcTool != nil && pkgsrcTool.Validity == Nowhere {
			// The tool must have been added too late to USE_TOOLS,
			// i.e. after bsd.prefs.mk has been included.
			ck.MkLine.Warnf("To use the tool ${%s} at load time, it has to be added to USE_TOOLS before including bsd.prefs.mk.", varname)
			return
		}
	}

	ck.MkLine.Warnf("The tool ${%s} cannot be used at load time.", varname)
	ck.MkLine.Explain(
		"To use a tool at load time, it must be declared in the package",
		"Makefile by adding it to USE_TOOLS.",
		"After that, bsd.prefs.mk must be included.",
		"Adding the tool to USE_TOOLS at any later time has no effect,",
		"which means that the tool can only be used at run time.",
		"That's the rule for the package Makefiles.",
		"",
		"Since any other .mk file can be included from anywhere else, there",
		"is no guarantee that the tool is properly defined for using it at",
		"load time (see above for the tricky rules).",
		"Therefore the tools can only be used at run time,",
		"except in the package Makefile itself.")
}

// checkVarUseWords checks whether a variable use of the form ${VAR}
// or ${VAR:modifiers} is allowed in a certain context.
func (ck *MkVarUseChecker) checkQuoting(vuc *VarUseContext) {
	if !G.Opts.WarnQuoting || vuc.quoting == VucQuotUnknown {
		return
	}

	varUse := ck.use
	vartype := ck.vartype

	needsQuoting := ck.MkLine.VariableNeedsQuoting(ck.MkLines, varUse, vartype, vuc)
	if needsQuoting == unknown {
		return
	}

	mod := varUse.Mod()

	// In GNU configure scripts, a few variables need to be passed through
	// the :M* modifier before they reach the configure scripts. Otherwise
	// the leading or trailing spaces will lead to strange caching errors
	// since the GNU configure scripts cannot handle these space characters.
	//
	// When doing checks outside a package, the :M* modifier is needed for safety.
	needMstar := (G.Pkg == nil || G.Pkg.vars.IsDefined("GNU_CONFIGURE")) &&
		matches(varUse.varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`)

	mkline := ck.MkLine
	if mod == ":M*:Q" && !needMstar {
		if !vartype.IsGuessed() {
			mkline.Notef("The :M* modifier is not needed here.")
		}

	} else if needsQuoting == yes {
		ck.checkQuotingQM(mod, needMstar, vuc)
	}

	if hasSuffix(mod, ":Q") && needsQuoting == no {
		ck.warnRedundantModifierQ(mod)
	}
}

func (ck *MkVarUseChecker) checkQuotingQM(mod string, needMstar bool, vuc *VarUseContext) {
	vartype := ck.vartype
	varname := ck.use.varname

	modNoQ := strings.TrimSuffix(mod, ":Q")
	modNoM := strings.TrimSuffix(modNoQ, ":M*")
	correctMod := modNoM + condStr(needMstar, ":M*:Q", ":Q")

	if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() {

		isSingleWordConstant := func() bool {
			if G.Pkg == nil {
				return false
			}

			varinfo := G.Pkg.redundant.vars[varname]
			if varinfo == nil || !varinfo.vari.IsConstant() {
				return false
			}

			value := varinfo.vari.ConstantValue()
			return len(ck.MkLine.ValueFields(value)) == 1
		}

		if vartype.IsList() && isSingleWordConstant() {
			// Do not warn in this special case, which typically occurs
			// for BUILD_DIRS or similar package-settable variables.

		} else if vartype.IsList() {
			ck.warnListVariableInWord()
		} else {
			ck.warnMissingModifierQInWord()
		}

	} else if mod != correctMod {
		if vuc.quoting == VucQuotPlain {
			ck.fixQuotingModifiers(correctMod, mod)
		} else {
			ck.warnWrongQuotingModifiers(correctMod, mod)
		}

	} else if vuc.quoting != VucQuotPlain {
		ck.warnModifierQInQuotes(mod)
	}
}

func (ck *MkVarUseChecker) warnListVariableInWord() {
	mkline := ck.MkLine

	mkline.Warnf("The list variable %s should not be embedded in a word.",
		ck.use.varname)
	mkline.Explain(
		"When a list variable has multiple elements, this expression expands",
		"to something unexpected:",
		"",
		"Example: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
		"",
		"\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/",
		"",
		"The first URL is missing the directory.",
		"To fix this, write",
		"\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
		"",
		"Example: -l${LIBS} expands to",
		"",
		"\t-llib1 lib2",
		"",
		"The second library is missing the -l.",
		"To fix this, write ${LIBS:S,^,-l,}.")
}

func (ck *MkVarUseChecker) warnMissingModifierQInWord() {
	mkline := ck.MkLine

	mkline.Warnf("The variable %s should be quoted as part of a shell word.",
		ck.use.varname)
	mkline.Explain(
		"This variable can contain spaces or other special characters.",
		"Therefore it should be quoted by replacing ${VAR} with ${VAR:Q}.")
}

func (ck *MkVarUseChecker) fixQuotingModifiers(correctMod string, mod string) {
	varname := ck.use.varname

	fix := ck.MkLine.Autofix()
	fix.Warnf("Please use ${%s%s} instead of ${%s%s}.", varname, correctMod, varname, mod)
	fix.Explain(
		seeGuide("Echoing a string exactly as-is", "echo-literal"))
	fix.Replace("${"+varname+mod+"}", "${"+varname+correctMod+"}")
	fix.Apply()
}

func (ck *MkVarUseChecker) warnWrongQuotingModifiers(correctMod string, mod string) {
	mkline := ck.MkLine
	varname := ck.use.varname

	mkline.Warnf("Please use ${%s%s} instead of ${%s%s} and make sure"+
		" the variable appears outside of any quoting characters.", varname, correctMod, varname, mod)
	mkline.Explain(
		"The :Q modifier only works reliably when it is used outside of any",
		"quoting characters like 'single' or \"double\" quotes or `backticks`.",
		"",
		"Examples:",
		"Instead of CFLAGS=\"${CFLAGS:Q}\",",
		"     write CFLAGS=${CFLAGS:Q}.",
		"Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
		"     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
		"",
		seeGuide("Echoing a string exactly as-is", "echo-literal"))
}

func (ck *MkVarUseChecker) warnModifierQInQuotes(mod string) {
	mkline := ck.MkLine

	mkline.Warnf("Please move ${%s%s} outside of any quoting characters.",
		ck.use.varname, mod)
	mkline.Explain(
		"The :Q modifier only works reliably when it is used outside of any",
		"quoting characters like 'single' or \"double\" quotes or `backticks`.",
		"",
		"Examples:",
		"Instead of CFLAGS=\"${CFLAGS:Q}\",",
		"     write CFLAGS=${CFLAGS:Q}.",
		"Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
		"     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
		"",
		seeGuide("Echoing a string exactly as-is", "echo-literal"))
}

func (ck *MkVarUseChecker) warnRedundantModifierQ(mod string) {
	varname := ck.use.varname

	bad := "${" + varname + mod + "}"
	good := "${" + varname + strings.TrimSuffix(mod, ":Q") + "}"

	fix := ck.MkLine.Line.Autofix()
	fix.Notef("The :Q modifier isn't necessary for ${%s} here.", varname)
	fix.Explain(
		"Many variables in pkgsrc do not need the :Q modifier since they",
		"are not expected to contain whitespace or other special characters.",
		"Examples for these \"safe\" variables are:",
		"",
		"\t* filenames",
		"\t* directory names",
		"\t* user and group names",
		"\t* tool names and tool paths",
		"\t* variable names",
		"\t* package names (but not dependency patterns like pkg>=1.2)")
	fix.Replace(bad, good)
	fix.Apply()
}

func (ck *MkVarUseChecker) checkBuildDefs() {
	varname := ck.use.varname

	if !G.Pkgsrc.UserDefinedVars.IsDefined(varname) || G.Pkgsrc.IsBuildDef(varname) {
		return
	}
	if ck.MkLines.buildDefs[varname] {
		return
	}
	if !ck.MkLines.once.FirstTimeSlice("BUILD_DEFS", varname) {
		return
	}

	ck.MkLine.Warnf("The user-defined variable %s is used but not added to BUILD_DEFS.", varname)
	ck.MkLine.Explain(
		"When a pkgsrc package is built, many things can be configured by the",
		"pkgsrc user in the mk.conf file.",
		"All these configurations should be recorded in the binary package",
		"so the package can be reliably rebuilt.",
		"The BUILD_DEFS variable contains a list of all these",
		"user-settable variables, so please add your variable to it, too.")
}

func (ck *MkVarUseChecker) checkDeprecated() {
	varname := ck.use.varname
	instead := G.Pkgsrc.Deprecated[varname]
	if instead == "" {
		instead = G.Pkgsrc.Deprecated[varnameCanon(varname)]
	}
	if instead == "" {
		return
	}

	ck.MkLine.Warnf("Use of %q is deprecated. %s", varname, instead)
}