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

Use first line number from LineNumberTable for JavaCodeUnit's sourceCodeLocation #344

Merged
merged 1 commit into from
Apr 15, 2020

Conversation

hankem
Copy link
Member

@hankem hankem commented Apr 10, 2020

So far, the sourceCodeLocation of JavaMembers does not contain a line number, as it cannot reliably be inferred from the byte code (cf. #75).

However, if the class file contains a LineNumberTable for a method, using the smallest line number from the LineNumberTable for the member's sourceCodeLocation – even if this probably corresponds to the first executable statement and not to the method header definition – seems to be more useful to me than using the fallback line number 0 in any case.

So for example, I would infer the line number 10 from the following byte code:

  void methodWithBodyStartingInLine10();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: bipush        11
        13: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: bipush        12
        21: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        24: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 16
        line 13: 24

(My example's method header is actually defined in line 9, but I prefer 10 as opposed to 0... 😉)

Note that I do even get a line number for an empty method from the following byte code:

  void emptyMethodDefinedInLine15();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 15: 0

Even if line numbers inferred in this way do not exactly point to the method header definition, they can be used to compare different methods (i.e., the ordering should be correct). This would allow for new rules like, for example (irrespective of whether I'd personally want to have them as arch rules or not):

With this pull request, line numbers are recorded for JavaCodeUnits (JavaMethods, JavaConstructors, JavaStaticInitializers) if the class file has a LineNumberTable.
(The reported line number 0 for JavaFields is unchanged, as it cannot be inferred from byte code.)

@ghost
Copy link

ghost commented Apr 10, 2020

Congratulations 🎉. DeepCode analyzed your code in 1.714 seconds and we found no issues. Enjoy a moment of no bugs ☀️.

👉 View analysis in DeepCode’s Dashboard

@codecholeric
Copy link
Collaborator

Nice, thanks a lot 😃
I still feel a little uneasy about the "heuristic over the thumb" but I agree that it is probably more useful than a constant 0 😉
It's particularly nice that you get this number also for void-methods with empty body 😄
This makes it at least pretty consistent between methods and constructors I guess.
Too bad there is no way to determine it for fields 😞
But regarding the heuristic, is there any way to distinguish this empty case when the line number is actually exact (at least as far as I could tell?) and the case where it is the first line of the body? Because if yes I would wonder if a best guess - 1 wouldn't be even better in that case, even though it might sometimes still be a little off? (e.g. if there is a comment in the first line)

@hankem
Copy link
Member Author

hankem commented Apr 12, 2020

I don't think there's a way to get what you'd consider exact... Consider this example:

17    void emptyMethodEndingInLine19() {
18
19    }

The auto-generated return (the only statement in the byte code) gets placed in line 19, presumably at the }.

If we had instead used

17    void emptyMethodEndingInLine19() {
18        return;
19    }

it would of course be in line 18... 🤷‍♂️

I would anyways refrain from playing any "line number – 1" trick, but just stick to the data that is actually there (to me, the only canonical options seem to be either the first or the last line number of the statements in the byte code), because

  • we don't know, as you already mention, whether "the first line of a statement – 1" really hits the method header declaration, due to, e.g.,
    • the method body starting with a comment (which can be arbitrarily long)
    • the method body starting with one or several empty lines
    • a code style demanding a method such as
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {

to be written as

    public void visit(
            int version,
            int access,
            String name,
            String signature,
            String superName,
            String[] interfaces
    ) {
  • simple, well defined behavior can IMO more easily be explained/understood
    (I'm sure that people will ask what the line number means...)
  • we don't pretend that we could solve a problem that we know we cannot. 😉

@codecholeric
Copy link
Collaborator

Okay, you have convinced me 😉 I agree, we can't solve it perfectly, and an educated guess close to the declaration is still a lot nicer than line number 0 which will definitely make you search for the declaration (in any non-trivial class). And yes, keeping it simple and directly providing the info from the bytecode seems to be the way to go. Too bad we have no estimate whatsoever for fields ☹️

Copy link
Collaborator

@codecholeric codecholeric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 😄, I have one small comment. Also I feel we should squash these two commits. Because adapting the tests feels to me like it should be part of the same commit. WDYT?

@hankem
Copy link
Member Author

hankem commented Apr 14, 2020

I feel we should squash these two commits.

Fair enough. (I just kept them separate as I wasn't sure whether we should make the existing failure report tests somehow ignore the line number of methods [which actually didn't work out easily] rather than encode the line number in the tests... 😉)

@hankem hankem force-pushed the sourceCodeLocation-of-code-units branch from 1cfbd8a to c6c0f38 Compare April 14, 2020 20:06
…for JavaCodeUnit's sourceCodeLocation

Signed-off-by: Manfred Hanke <Manfred.Hanke@tngtech.com>
@codecholeric codecholeric force-pushed the sourceCodeLocation-of-code-units branch from c6c0f38 to 61e140f Compare April 15, 2020 16:24
@codecholeric codecholeric merged commit 72b5ad6 into master Apr 15, 2020
@codecholeric codecholeric deleted the sourceCodeLocation-of-code-units branch April 15, 2020 17:29
codecholeric added a commit that referenced this pull request Feb 21, 2021
…odeLocation #344

So far, the `sourceCodeLocation` of `JavaMember`s does not contain a line number, as it cannot reliably be inferred from the byte code (cf. #75).

However, if the class file contains a [`LineNumberTable`](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.12) for a method, using the smallest line number from the `LineNumberTable` for the member's `sourceCodeLocation` – even if this probably corresponds to the first executable statement and not to the method header definition – seems to be more useful to me than using the fallback line number `0` in any case.

So for example, I would infer the line number `10` from the following byte code:
```
  void methodWithBodyStartingInLine10();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: bipush        11
        13: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        16: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: bipush        12
        21: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        24: return
      LineNumberTable:
        line 10: 0
        line 11: 8
        line 12: 16
        line 13: 24
```
(My example's method header is actually defined in line 9, but I prefer 10 as opposed to 0... 😉)

Note that I do even get a line number for an empty method from the following byte code:
```
  void emptyMethodDefinedInLine15();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 15: 0
```

Even if line numbers inferred in this way do not exactly point to the method header definition, they can be used to  compare different methods (i.e., the ordering should be correct). This would allow for new rules like, for example (irrespective of whether I'd personally want to have them as arch rules or not):
* Public methods are defined _before_ private methods.
* `equals`, `hashCode` and `toString` are not generated by a framework (or a developer... 🙈) in the _same_ line (cf. #337)

With this pull request, line numbers are recorded for `JavaCodeUnit`s (`JavaMethod`s, `JavaConstructor`s, `JavaStaticInitializer`s) if the class file has a `LineNumberTable`.
(The reported line number `0` for `JavaField`s is unchanged, as it cannot be inferred from byte code.)
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

Successfully merging this pull request may close these issues.

2 participants