Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: add Addr keyword to get pointer type from non-pointer type #129

Closed
sebastien-rosset opened this issue Nov 18, 2020 · 31 comments
Closed

Comments

@sebastien-rosset
Copy link
Contributor

sebastien-rosset commented Nov 18, 2020

Suppose we want to match a struct initialization statement, but only when the struct implements interface mypkg.MyInterface. For example:

a := &mypkg.Type1{}
b := mypkg.Type2{}
c := mypkg.Type3{}
myfunc("abc", mypkg.Type3{}, "abc", &mypkg.Type4{})

Here is a first attempt at writing a ruleguard checker:

Matcher(`$p.$x{$_*}`).
  Where(m["x"].Type.Implements(`mypkg.MyInterface`))

However this does not work because ExprType.Implements() return true only if the ExprType is a pointer to a type. Is there a way to convert to a pointer, or do we need to add a new keyword to get the pointer to a type given an ExprType? Maybe something like this?

Matcher(`$p.$x{$_*}`).
  Where(m["x"].Type.Addr().Implements(`mypkg.MyInterface`))

As a side note, my observation is that Matcher("$p.$x{$_*}") or Matcher("$x{$_*}") patterns are not greedy, which makes sense but is not necessarily obvious. In the a := &mypkg.Type1{} example, "m[x].Type" is the type mypkg.Type1, not *mypkg.Type1.

@quasilyte
Copy link
Owner

quasilyte commented Nov 27, 2020

Sorry for the late response.

The common approach for your kind of issue is to use 2 patterns:

Matcher(`&$p.$x{$_*}`,
        `$p.$x{$_*}`).
  Where(m["x"].Type.Implements(`mypkg.MyInterface`))

It also looks like $p is not needed there. This should probably be identical:

Matcher(`&$x{$_*}`,
        `$x{$_*}`).
  Where(m["x"].Type.Implements(`mypkg.MyInterface`))

I'll try your example tomorrow and see if I can provide a better answer.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Nov 28, 2020

With the following pattern:

Matcher(`&$x{$_*}`).
  Where(m["x"].Type.Implements(`mypkg.MyInterface`))

That will match against code a := &mypkg.Type1{} but isn't m["x"] going to be just mypkg.Type1, which is just the struct, not a pointer, and therefore Implements() returns false (because in order to implement the interface the value must be a pointer)?

BTW, coming laterally from the concept of greedy/non greedy regular expression, it would be great to explain how greedy the matches are. For example, with the code &mypkg.Type1{} there is the package name and the struct name separated by a dot. If the matching pattern is &$x{$_*} will $x greedily match against mypkg.Type1?

@quasilyte
Copy link
Owner

That will match against code a := &mypkg.Type1{} but isn't m["x"] going to be just mypkg.Type1, which is just the struct, not a pointer, and therefore Implements() returns false (because in order to implement the interface the value must be a pointer)?

You're right. On the other hand, we may allow using $$ variable (an entire match node) to be used in filters. As a whole, that literal will be *T typed. And the form without & will be T typed.

The AST pattern matching details rely on the gogrep. I may try to document important things and send a PR to Daniel so the gogrep users can use it more easily, but I believe such details are "implementation-defined" and complex patterns may or may not work as you might intend them to work. The best solution here is to test rules and see whether they behave correctly. #91 can help here.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Dec 10, 2020

You're right. On the other hand, we may allow using $$ variable (an entire match node) to be used in filters. As a whole, that literal will be *T typed. And the form without & will be T typed.

I understand how that would solve this scenario: a := &mypkg.Type1{}
But how about? b := mypkg.Type2{}. The type will be mypkg.Type2, but we need a *mypkg.Type2 type when invoking Implements(), otherwise Implements() will return false.

@quasilyte
Copy link
Owner

Could you share an example of a warning text for this pattern so I can understand it better?

@sebastien-rosset
Copy link
Contributor Author

Could you share an example of a warning text for this pattern so I can understand it better?

Sure. An example would be Replace struct initialization mypackage.Xyz{} with call to factory method mypackage.NewXyz().
I have put more details in #137, which is a proposal to resolve this issue.

@sebastien-rosset sebastien-rosset changed the title Suggestion: add PtrTo keyword or equivalent Suggestion: add Addr keyword or equivalent Dec 31, 2020
@sebastien-rosset sebastien-rosset changed the title Suggestion: add Addr keyword or equivalent Suggestion: add Addr keyword to get pointer type from non-pointer type Dec 31, 2020
@quasilyte
Copy link
Owner

quasilyte commented Jan 1, 2021

I see the picture now, thank you.

So, you want to match literals that "may" implement an interface even if they don't in the way they're written inside the code. This means we're adding a concept of types manipulation to the engine. This is a novel concept, hence it took some time for me to grasp it.

We can add new methods to the Type, but I think it's an example where the current model doesn't shine.

If we could write some kinds of custom predicates, this would be a more general solution:

func implementsStringer(ctx dsl.Context, typ types.Type) bool {
	return ctx.Implements(typ, `fmt.Stringer`) ||
		ctx.Implements(types.NewPointer(typ), `fmt.Stringer`)
}

func stringerLiteral(m dsl.Matcher) {
	m.Match(`$x{$*_}`).
		Where(m["x"].Type.Check(implementsStringer)).
		Report(`$x implements fmt.Stringer`)
}

Where ctx holds all data to make it easier to query types loaded by the analyzer and types.Type is an actual go/types representation of a type. The implementsStringer is presumably a normal Go function that can perform the required operation.

The problem is: there is no Go interpreter inside the ruleguard. I thought about using yaegi, but I'm not 100% convinced yet. Not that the performance is the only concern (it would make custom filters many times slower), but also it's a big dependency that I'm not to keen to take right now. I may be wrong here.

If ruleguard rules were 100% normal Go code and were executed with go run we would not have such a problem at all. But then we would need to recompile more and golangci-lint integration would be nearly impossible (as you would have to recompile golangci-lint to update rules, which is awkward).

Having custom extension filters looks like a must-have to create serious checks. The current mechanisms only good enough for simple things and it was, honestly speaking, supposed to be this way. Complicated checks were to be written as normal analyzers/linters. Not saying I wouldn't like to extend what ruleguard should be capable of though.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 1, 2021

In principle, it would be nice to be able to invoke a go interpreter or compiled go code through reflection, though as you mention one downside is performance and memory utilization. While running ruleguard I've noticed memory utilization can be high, I need to spend some time understanding why. Presumably it would be harder to debug memory usage if/when arbitrary functions can be invoked.

Or, how about supporting go plugins?

For this specific need, the changes (PR #137 137) are small and localized.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 3, 2021

How about supporting the following grammar: Where(m["x"].Type.Check("foo.so", "symbol_name"))

where "foo.so" is a go plugin and symbol_name is the name of a exported func(ctx dsl.Context, typ types.Type) bool defined in the plugin? Due to current plugin limitations, that only works on Linux, FreeBSD, and macOS, but there are no new dependencies and it's fairly simple to implement.

Similarly, there could be Where(m["x"].Node.Check("foo.so", "symbol_name"))

@quasilyte
Copy link
Owner

Plugins could be one way to solve it. I don't dislike them that much, but they might be too much for some simple cases.

I started to work on a solution, but I need a week or two to show some results.

@quasilyte
Copy link
Owner

quasilyte commented Jan 5, 2021

On my local branch, this code can now be used:

package gorules

import (
	"github.com/quasilyte/go-ruleguard/dsl"
	"github.com/quasilyte/go-ruleguard/dsl/types"
)

// Note: the exact dsl.VarFilterContext API may change; for example, Type()
// may become a field instead of a method.
func implementsStringer(ctx *dsl.VarFilterContext) bool {
	// GetInterface() returns *types.Interface for a given FQN
	stringer := ctx.GetInterface(`fmt.Stringer`)
	return types.Implements(ctx.Type(), stringer) ||
		types.Implements(types.NewPointer(ctx.Type()), stringer)
}

func stringerLiterals(m dsl.Matcher) {
	m.Match(`$x{$*_}`).
		Where(m["x"].Filter(implementsStringer)).
		Report("$x implements stringer")
}

github.com/quasilyte/go-ruleguard/dsl/types copies API from go/types as close as possible. It calls these functions under the hood, but it strips off things that make no sense (like types.Config for typechecking).

Custom filter functions are more low level, but they still can use helpers like ctx.GetInterface that will do the right thing to lookup the queries type.

If we run it on this Go file:

package target

type byValue struct{}
type byPtr struct{}

func (*byPtr) String() string  { return "" }
func (byValue) String() string { return "" }

func f() {
	_ = &byValue{}
	_ = byValue{}
	_ = &byPtr{}
	_ = byPtr{} // does not implement fooer, but we still want it
}

We'll get this output:

example.go:10:7: stringerLiterals: byValue implements stringer
example.go:11:6: stringerLiterals: byValue implements stringer
example.go:12:7: stringerLiterals: byPtr implements stringer
example.go:13:6: stringerLiterals: byPtr implements stringer

Under the hood, there is a simple bytecode interpreter for a Go subset. It should be good enough for filters.

Performance-wise, it's better than yaegi and the memory footprint is quite low.

func f() int {
  x := 100
  if x == 1 {
    x = 10
  } else if x == 2 {
    x = 20
  } else {
    x = 30
  }
  return x
}

yaegi 957 ns/op       288 B/op         7 allocs/op
eval  151 ns/op        32 B/op         1 allocs/op

Calling "native" Go functions from the interpreted code (benchmark does 2 function calls):

yaegi 2466 ns/op       560 B/op        14 allocs/op
eval  144 ns/op        32 B/op         1 allocs/op

The exact numbers are irrelevant, but it shows that it's quite good on the performance side even though I did 0 optimization efforts and don't use unsafe for the interpreter. We can optimize it later if we ever want to. But I think it's efficient enough for what it's going to be used for.

I need a few more days to finalize it (if nothing distracts me from it, of course).

quasilyte added a commit that referenced this issue Jan 6, 2021
`dsl/types` is a wrapper around `go/types` that provides the
minimal interface that is useful for custom filter.

We may add `dsl/ast` later to mimic `go/ast` as well for
node-related custom filters.

See #129 (comment)
to get more context on what this is about.
quasilyte added a commit that referenced this issue Jan 6, 2021
`dsl/types` is a wrapper around `go/types` that provides the
minimal interface that is useful for custom filter.

We may add `dsl/ast` later to mimic `go/ast` as well for
node-related custom filters.

See #129 (comment)
to get more context on what this is about.
quasilyte added a commit that referenced this issue Jan 6, 2021
`dsl/types` is a wrapper around `go/types` that provides the
minimal interface that is useful for custom filter.

We may add `dsl/ast` later to mimic `go/ast` as well for
node-related custom filters.

See #129 (comment)
to get more context on what this is about.
quasilyte added a commit that referenced this issue Jan 6, 2021
Custom filters are simple Go functions defined in ruleguard rule files
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
matched rule needs to apply its filters. The performance is
good enough for now (faster than yaegi) and can be further
improved in future.

There are various limitations in what can be used in custom
filters and what not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Type.Check(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}

Custom filters are more flexible predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.
quasilyte added a commit that referenced this issue Jan 6, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).

Also fixes #167 and #170
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).

Also fixes #167 and #170
quasilyte added a commit that referenced this issue Jan 8, 2021
that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).

Also fixes #167 and #170
quasilyte added a commit that referenced this issue Jan 8, 2021
#173)

that can be used as Where clause predicates.

They're compiled to a bytecode that is then interpreted when
the matched rule needs to apply its filters. The performance is
good enough for now (faster than `yaegi`) and can be further
improved in the future.

There are various limitations in what can be used in custom
filters and what's not. It's not well-documented right now,
but the compile errors try to be helpful.
What can be compiled will be executed like a normal Go would.

One use case for this new feature can be found in #129

Here is the solution to #129 which is not possible:

```go
	func implementsStringer(ctx *dsl.VarFilterContext) bool {
		stringer := ctx.GetInterface(`fmt.Stringer`)
		return types.Implements(ctx.Type, stringer) ||
			types.Implements(types.NewPointer(ctx.Type), stringer)
	}

	func stringerLiteral(m dsl.Matcher) {
		m.Match(`$x{$*_}`).
			Where(m["x"].Filter(implementsStringer)).
			Report(`$x implements fmt.Stringer`)
	}
```

Custom filters are more flexible than predefined filters, but they
generally require more coding. As a rule of thumb: they should be
used only when there is no matching builtin filter.

Note: right now many simple operations, like integer multiplication,
are not implemented. They can be added trivially, but I wanted to
present a working concept with a minimal amount of code for this first PR.
Upcoming changes that extend the supported features set are going
to be easier to review.

It's also possible to allow calling byte-compiled functions from other byte-compiled functions.
But it's not there yet (I also need examples where it can be applied to get a better
understanding of the subject).

Also fixes #167 and #170
@quasilyte
Copy link
Owner

@sebastien-rosset
Copy link
Contributor Author

Thanks. Let me test with my use case and then I can close both the issue and the PR.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 14, 2021

How should I test the latest (unreleased) software? Sorry this seems like a trivial question and previously I was able to test, but now I'm getting errors.

I've tried:

GO111MODULE=on go get -v -u github.com/quasilyte/go-ruleguard/...@master

When I run ruleguard I get the following error:

ruleguard: load rules: parse rules file: typechecker error: rules.go:5:8:
 could not import github.com/quasilyte/go-ruleguard/dsl (can't find import: "github.com/quasilyte/go-ruleguard/dsl")

I know previously it was github.com/quasilyte/go-ruleguard/dsl/fluent.

I've also tried:

git clone [email protected]:quasilyte/go-ruleguard.git
cd go-ruleguard
go install ./...

Then when I run ruleguard I get the following error:

ruleguard: load rules: parse rules file: typechecker error: rules.go:12:26: cannot use ctx.Type (variable of type types.Type) as types.Type value in argument to types.Implements: wrong type for method Underlying (
have func() github.com/quasilyte/go-ruleguard/dsl/types.Type,
want func() github.com/quasilyte/go-ruleguard/dsl/types.Type)

The printed have vs want is actually the same.

Here is the rules.go file:

// +build ignore

package gorules

import (
	"github.com/quasilyte/go-ruleguard/dsl"
	"github.com/quasilyte/go-ruleguard/dsl/types"
)

func implementsStringer(ctx *dsl.VarFilterContext) bool {
	stringer := ctx.GetInterface(`fmt.Stringer`)
	return types.Implements(ctx.Type, stringer) ||
		types.Implements(types.NewPointer(ctx.Type), stringer)
}

@quasilyte
Copy link
Owner

You can try the latest release binaries (v0.3.0).

In order to make ruleguard find dsl package, there are 2 options:

  • If you're inside a Go module, install dsl package with go get
  • If you're outisde of a Go module, it should be available via GOPATH

Running the go list --json github.com/quasilyte/go-ruleguard/dsl could probably give us some insights.

You can check out the _test/install to see the supported installation options.

Then when I run ruleguard I get the following error

That error may be related to the fact that we get different types packages somehow. It needs debugging, but I need to reproduce this case first. I'll try the exact steps you've mention in Docker.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 15, 2021

I've recompiled go and then used it to compile ruleguard. I've added debugging info in https://github.com/golang/go/blob/master/src/go/types/predicates.go#L147. When ruleguard executes, src/go/types/operand.go.assignableTo() is invoked and the printed error is exactly the same for have vs want.

have func() github.com/quasilyte/go-ruleguard/dsl/types.Type
want func() github.com/quasilyte/go-ruleguard/dsl/types.Type

src/go/types/operand.go.assignableTo() invokes identical0 which returns false here: https://github.com/golang/go/blob/master/src/go/types/predicates.go#L227

This happens when ruleguard compares two types.Underlying() method signatures: https://github.com/quasilyte/go-ruleguard/blob/master/dsl/types/types.go#L14

I suspect this has to do with vendoring. If I run ruleguard with a basic test, everything works. But in my real project, there is vendoring. I will add more debug info.

Here is the stack trace when the comparison fails:

goroutine 7354 [running]:
runtime/debug.Stack(0xc0000b6050, 0x2, 0xc000cc0f20)
	/home/serosset/goroot/src/github.com/golang/go/src/runtime/debug/stack.go:24 +0x9f
runtime/debug.PrintStack()
	/home/serosset/goroot/src/github.com/golang/go/src/runtime/debug/stack.go:16 +0x25
go/types.(*Checker).identical0(0xc00a94dc20, 0x892f40, 0xc00ab079f0, 0x892f40, 0xc0057f8500, 0x1, 0x0, 0x837f00)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/predicates.go:319 +0x140a
go/types.(*Checker).identical0(0xc00a94dc20, 0x8930c0, 0xc00ab27820, 0x8930c0, 0xc0057e1fe0, 0x1, 0x0, 0xc00d94d100)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/predicates.go:213 +0x2e5
go/types.(*Checker).identical0(0xc00a94dc20, 0x892fc0, 0xc006bccc90, 0x892fc0, 0xc0057e59e0, 0xc00ab07b01, 0x0, 0xc00d94d300)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/predicates.go:233 +0x1087
go/types.(*Checker).identical(...)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/predicates.go:120
go/types.(*operand).assignableTo(0xc005571740, 0xc00a94dc20, 0x892f40, 0xc00ab079f0, 0xc00d94d3b0, 0x60d83a)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/operand.go:269 +0x496
go/types.(*Checker).assignment(0xc00a94dc20, 0xc005571740, 0x892f40, 0xc00ab079f0, 0xc00724bf80, 0x1c)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/assignments.go:60 +0x1b3
go/types.(*Checker).argument(0xc00a94dc20, 0xc006bcd170, 0x0, 0xc005571740, 0x0, 0xc00724bf80, 0x1c)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/call.go:313 +0x130
go/types.(*Checker).arguments(0xc00a94dc20, 0xc005571740, 0xc005570500, 0xc006bcd170, 0xc00d94d610, 0x2)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/call.go:255 +0x1b7
go/types.(*Checker).call(0xc00a94dc20, 0xc005571740, 0xc005570500, 0xc005ece900)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/call.go:71 +0x65e
go/types.(*Checker).exprInternal(0xc00a94dc20, 0xc005571740, 0x896e00, 0xc005570500, 0x0, 0x0, 0xc009fcb850)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:1471 +0x1db0
go/types.(*Checker).rawExpr(0xc00a94dc20, 0xc005571740, 0x896e00, 0xc005570500, 0x0, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:987 +0x85
go/types.(*Checker).multiExpr(0xc00a94dc20, 0xc005571740, 0x896e00, 0xc005570500)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:1604 +0x58
go/types.(*Checker).expr(0xc00a94dc20, 0xc005571740, 0x896e00, 0xc005570500)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:1598 +0x49
go/types.(*Checker).binary(0xc00a94dc20, 0xc005571740, 0xc006aff830, 0x896e00, 0xc005570500, 0x896e00, 0xc0055705c0, 0x23)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:778 +0x8a
go/types.(*Checker).exprInternal(0xc00a94dc20, 0xc005571740, 0x896d40, 0xc006aff830, 0x0, 0x0, 0xc00d94e750)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:1505 +0x1d5b
go/types.(*Checker).rawExpr(0xc00a94dc20, 0xc005571740, 0x896d40, 0xc006aff830, 0x0, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:987 +0x85
go/types.(*Checker).multiExpr(0xc00a94dc20, 0xc005571740, 0x896d40, 0xc006aff830)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/expr.go:1604 +0x58
go/types.(*Checker).initVars.func1(0xc005571740, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/assignments.go:209 +0x5d
go/types.unpack(0xc00d94e910, 0x1, 0x0, 0x0, 0x8, 0x897200)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/call.go:189 +0x66
go/types.(*Checker).initVars(0xc00a94dc20, 0xc0074f4558, 0x1, 0x1, 0xc009fcb160, 0x1, 0x1, 0x1e34a0c)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/assignments.go:209 +0xe5
go/types.(*Checker).stmt(0xc00a94dc20, 0x0, 0x8974c0, 0xc00ab26580)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/stmt.go:447 +0x3578
go/types.(*Checker).stmtList(0xc00a94dc20, 0x0, 0xc00ab265a0, 0x2, 0x2)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/stmt.go:120 +0xd1
go/types.(*Checker).funcBody(0xc00a94dc20, 0xc005615b60, 0xc00724b580, 0x12, 0xc006bcda10, 0xc006aff860, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/stmt.go:42 +0x21c
go/types.(*Checker).funcDecl.func1()
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/decl.go:662 +0x67
go/types.(*Checker).processDelayed(0xc00a94dc20, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/check.go:290 +0x3e
go/types.(*Checker).checkFiles(0xc00a94dc20, 0xc00d94f788, 0x1, 0x1, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/check.go:266 +0xd0
go/types.(*Checker).Files(...)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/check.go:249
go/types.(*Config).Check(0xc005570980, 0x8210cc, 0x7, 0xc0057b04c0, 0xc00d94f788, 0x1, 0x1, 0xc00ab06e60, 0x0, 0xc00d94f788, ...)
	/home/serosset/goroot/src/github.com/golang/go/src/go/types/api.go:361 +0x151
github.com/quasilyte/go-ruleguard/ruleguard.(*rulesParser).ParseFile(0xc00d94f8d8, 0x7fffe3cb5b8d, 0x27, 0x890080, 0xc006aff5f0, 0x203001, 0xc00d94f8a0, 0x4d356f)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/ruleguard/parser.go:111 +0x6f5
github.com/quasilyte/go-ruleguard/ruleguard.(*engine).Load(0xc005798550, 0xc005789a70, 0x7fffe3cb5b8d, 0x27, 0x890080, 0xc006aff5f0, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/ruleguard/engine.go:40 +0x219
github.com/quasilyte/go-ruleguard/ruleguard.(*Engine).Load(...)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/ruleguard/ruleguard.go:34
github.com/quasilyte/go-ruleguard/analyzer.newEngine(0x10, 0x10, 0x7c50c0)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/analyzer/analyzer.go:210 +0x713
github.com/quasilyte/go-ruleguard/analyzer.prepareEngine(0x0, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/analyzer/analyzer.go:150 +0xab
github.com/quasilyte/go-ruleguard/analyzer.runAnalyzer(0xc000125d40, 0xc000125d40, 0x0, 0x0, 0x0)
	/home/serosset/goroot/src/github.com/quasilyte/go-ruleguard/analyzer/analyzer.go:77 +0x26
golang.org/x/tools/go/analysis/internal/checker.(*action).execOnce(0xc0025d4320)
	/home/serosset/goroot/pkg/mod/golang.org/x/[email protected]/go/analysis/internal/checker/checker.go:690 +0x87e
sync.(*Once).doSlow(0xc0025d4320, 0xc000702790)
	/home/serosset/goroot/src/github.com/golang/go/src/sync/once.go:66 +0xec
sync.(*Once).Do(...)
	/home/serosset/goroot/src/github.com/golang/go/src/sync/once.go:57
golang.org/x/tools/go/analysis/internal/checker.(*action).exec(0xc0025d4320)
	/home/serosset/goroot/pkg/mod/golang.org/x/[email protected]/go/analysis/internal/checker/checker.go:579 +0x65
golang.org/x/tools/go/analysis/internal/checker.execAll.func1(0xc0025d4320)
	/home/serosset/goroot/pkg/mod/golang.org/x/[email protected]/go/analysis/internal/checker/checker.go:567 +0x34
created by golang.org/x/tools/go/analysis/internal/checker.execAll
	/home/serosset/goroot/pkg/mod/golang.org/x/[email protected]/go/analysis/internal/checker/checker.go:573 +0x125

@sebastien-rosset
Copy link
Contributor Author

Actually I'm not so sure about vendoring being the problem. I discovered go-ruleguard/internal/mvdan.cc/gogrep/parse.go has calls to AddFile(). I was hoping maybe I would see duplicate entries for github.com/quasilyte/go-ruleguard/dsl/types.Type, but that didn't go anywhere. Let me know if you have a pointer on how to help troubleshoot this.

@quasilyte
Copy link
Owner

quasilyte commented Jan 16, 2021

I would start my attempt by trying to create a docker test that gives the universal reproducer in clean environment.

If it can't be reproduced there, it must be an environment issue which would require us to guess what needs to be fixed (and maybe add these fixing steps to the documentation or some kind of FAQ).

Could you share your go env output? It might be helpful.

I'm planning to give it a try this weekend.

@sebastien-rosset
Copy link
Contributor Author

Here: https://github.com/quasilyte/go-ruleguard/blob/master/go.mod#L7
I see you depend on version v0.0.0-20210106184943-e47d54850b18, which was committed on Jan 6 2021, but subsequently there were other commits under https://github.com/quasilyte/go-ruleguard/tree/master/dsl. Shouldn't the go.mod file be update to use the latest?

As an aside, ruleguard -V outputs ruleguard: unsupported flag value: -V=true

@sebastien-rosset
Copy link
Contributor Author

I have created a few Dockerfiles here: https://github.com/sebastien-rosset/ruleguard-test. For example:

FROM golang:1.15.6
WORKDIR /go/src/app
COPY . .
# Install ruleguard as documented at https://github.com/quasilyte/go-ruleguard
RUN GO111MODULE=on go get -v github.com/quasilyte/go-ruleguard/...
CMD [ "ruleguard", "-rules", "rules.go", "."]
> docker build -t ruleguard-test . ; docker run  --rm ruleguard-test
ruleguard: load rules: parse rules file: typechecker error: rules.go:6:3: could not import github.com/quasilyte/go-ruleguard/dsl (can't find import: "github.com/quasilyte/go-ruleguard/dsl")
```__

@quasilyte
Copy link
Owner

quasilyte commented Jan 20, 2021

As an aside, ruleguard -V outputs ruleguard: unsupported flag value: -V=true

That flag comes from the analysis framework (https://github.com/golang/tools/tree/master/go/analysis/internal/analysisflags). I have no idea why it doesn't work or what it's supposed to do. :) I think it expects an argument like -V=full but it still will not work.

Shouldn't the go.mod file be update to use the latest?

In theory, yes. But it's probably only relevant for running the tests. The DSL package should be a dependency of a package that is being checked (because we do a run-time based package importing, the package lookup depends on what go list would discover). The other option is to install the latest version to the GOPATH.

I'll investigate your reproducer today. Thank you for the efforts! It really helps.

@quasilyte
Copy link
Owner

OK, now I see what's going on.

There are 2 main ways to use ruleguard:

  1. Global installation with GOPATH. It's not a recommended option, but it may be convenient for some users. To do that, no GO111MODULE=on should be used during go get. So, removing the GO111MODULE should fix your problem there.

  2. The recommended way is available for projects that have go.mod file. They should install dsl package as direct (inderect is also OK, but it will be removed by go mod tidy) dependency. So, to make https://github.com/sebastien-rosset/ruleguard-test work, you probably need to do go mod init there.

The installation instruction should be updated I guess.

@quasilyte
Copy link
Owner

quasilyte commented Jan 24, 2021

I've updated the installation instructions.

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 24, 2021

Thank you! Now I'll try to reproduce wrong type for method Underlying (have func() github.com/quasilyte/go-ruleguard/dsl/types.Type, want func() github.com/quasilyte/go-ruleguard/dsl/types.Type) in a Dockerfile

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 25, 2021

It looks like the installation instructions for gocritic need to be updated too. With GO111MODULE=on and invoking go get outside a module, the errors below are printed.

I have updated the Dockerfiles for test purpose at https://github.com/sebastien-rosset/ruleguard-test

GO111MODULE=on go get -v -u github.com/go-critic/go-critic/cmd/gocritic

# github.com/go-critic/go-critic/checkers
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:60:18: undefined: ruleguard.GoRuleSet
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:68:16: undefined: ruleguard.ParseRules
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:76:11: undefined: ruleguard.MergeRuleSets
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:84:14: undefined: ruleguard.GoRuleSet
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:92:10: undefined: ruleguard.Context
/go/pkg/mod/github.com/go-critic/[email protected]/checkers/ruleguard_checker.go:108:9: undefined: ruleguard.RunRules

@sebastien-rosset
Copy link
Contributor Author

I've noticed if I run go mod vendor in a project then invoke ruleguard, the following error is returned:

ruleguard: load rules: parse rules file: typechecker error: rules.go:7:3:
could not import github.com/quasilyte/go-ruleguard/dsl/types
(can't find import: "github.com/quasilyte/go-ruleguard/dsl/types")

Dockerfile:
https://github.com/sebastien-rosset/ruleguard-test/blob/master/Dockerfile.vendored

@sebastien-rosset
Copy link
Contributor Author

sebastien-rosset commented Jan 26, 2021

@quasilyte , I have narrowed the problem to help troubleshoot wrong type for method Underlying (have func() github.com/quasilyte/go-ruleguard/dsl/types.Type, want func() github.com/quasilyte/go-ruleguard/dsl/types.Type)

I've noticed the following:

  1. Invoke ruleguard with multiple input rule files, I get the above error.
  2. Invoke ruleguard with a single rule file, then everything works.
- ruleguard -rules $(echo gorules/*.go | tr ' ' ',') ./...
+ ruleguard -rules rules.go ./...

In parser.go I've added log statements to list the files in the p.ctx.Fset. I've noticed in scenario (1), the file github.com/quasilyte/go-ruleguard/dsl/types/types.go is present twice in the p.ctx.Fset. This could explain why the comparison fails (?)

UPDATE: I was able to provide a minimal test to reproduces the issue. It turns out the order of the rule file names on the command line matters (everything else being the same).

- ruleguard -rules log-rule.go,worker-rule.go,string-rule.go .  # FAIL
+ ruleguard -rules worker-rule.go,string-rule.go,log-rule.go . # SUCCESS

Here is the Dockerfile that outputs "wrong type" error: https://github.com/sebastien-rosset/ruleguard-test/blob/master/Dockerfile.wrongtype
This Dockerfile is almost the same, the only difference is the order of the rule files on the command line argument. This test succeeds: https://github.com/sebastien-rosset/ruleguard-test/blob/master/Dockerfile.multiplerules

@sebastien-rosset
Copy link
Contributor Author

This thread has become too long. Addr is no longer needed, since there is now a better equivalent. Let me close this issue and open a separate one to track the typecheck issue.

@quasilyte
Copy link
Owner

This thread has become too long. Addr is no longer needed, since there is now a better equivalent. Let me close this issue and open a separate one to track the typecheck issue.

Good idea. 👍

@sebastien-rosset
Copy link
Contributor Author

Closing this issue. Fixed with #171, #173 and other PRs.

@quasilyte
Copy link
Owner

Even with custom filters, it's still somewhat cumbersome to solve this issue.
I came up with #331 idea. Not sure I want to implement it yet, but it seems to be a recurring problem for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants