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

AST nodes for an empty script contains incorrect line & column numbers in Eclipse #252

Closed
darxriggs opened this issue Jan 2, 2017 · 15 comments

Comments

@darxriggs
Copy link

Issue

For the following empty script the line & column numbers are incorrect for the MethodNode and BlockStatement of the generated run method. The values are correct for the generated main method.

Issue.groovy:

package com.example

Behaviour

Actual

The AST value in Eclipse for lineNumber, lastLineNumber, columnNumber and lastColumnNumber is different than for Groovy on the commandline.

Expected

The AST value in Eclipse for lineNumber, lastLineNumber, columnNumber and lastColumnNumber is -1 (the same as for Groovy on the commandline).

Steps to reproduce

In "Groovy AST Viewer" (Eclipse)

  • open file Issue.groovy
  • open "Window -> Show View -> Groovy -> Groovy AST Viewer"
  • inside "Groovy AST Viewer" open "classes -> com.example.Issue -> methods -> org.codehaus.groovy.ast.MethodNode-run"

In Groovy Console (Eclipse and commandline)

  • run "groovyConsole" on Issue.groovy
    • in Eclipse: right mouse button on Issue.groovy -> "Run As -> Groovy Console"
    • on commandline: groovyConsole Issue.groovy
  • inside "Groovy Console" select "Script -> Inspect Ast"
  • inside "Groovy AST Browser" select "ClassNode -> Methods -> MethodNode - run"

Environment

Tested in Eclipse

  • Eclipse Mars 4.5.2 + Groovy-Eclipse 2.9.2.xx-201701010228-e45 (Groovy 2.0.8 / 2.1.9 / 2.2.2 / 2.3.11 / 2.4.7)
  • Eclipse Indigo 3.7 + Groovy-Eclipse 2.9.1.xx-201411061404-e37-RELEASE (Groovy 2.0.7 / 2.1.8 / 2.2.2 / 2.3.7)

Tested on commandline

  • Groovy 2.3.7 / 2.4.7

Details

When running groovyConsole from commandline the values in question are:

MethodNode - run

  • lineNumber: -1
  • lastLineNumber: -1
  • columnNumber: -1
  • lastColumnNumber: -1
  • code: BlockStatement
    • lineNumber: -1
    • lastLineNumber: -1
    • columnNumber: -1
    • lastColumnNumber: -1

When running groovyConsole from Eclipse and selecting "MethodNode - run" the UI throws this exception:

Exception in thread "AWT-EventQueue-0" 
java.lang.IllegalArgumentException: bad position: 20
	at javax.swing.text.JTextComponent.setCaretPosition(JTextComponent.java:1678)
	at javax.swing.text.JTextComponent$setCaretPosition$0.call(Unknown Source)
	at groovy.inspect.swingui.AstBrowser$_run_closure5.doCall(AstBrowser.groovy:218)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
	at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1024)
	at groovy.lang.Closure.call(Closure.java:414)
	at org.codehaus.groovy.runtime.ConvertedClosure.invokeCustom(ConvertedClosure.java:54)
	at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:124)
	at com.sun.proxy.$Proxy10.valueChanged(Unknown Source)
	at javax.swing.JTree.fireValueChanged(JTree.java:2924)
	at javax.swing.JTree$TreeSelectionRedirector.valueChanged(JTree.java:3383)
	at javax.swing.tree.DefaultTreeSelectionModel.fireValueChanged(DefaultTreeSelectionModel.java:634)
	at javax.swing.tree.DefaultTreeSelectionModel.notifyPathChange(DefaultTreeSelectionModel.java:1092)
	at javax.swing.tree.DefaultTreeSelectionModel.setSelectionPaths(DefaultTreeSelectionModel.java:293)
	at javax.swing.tree.DefaultTreeSelectionModel.setSelectionPath(DefaultTreeSelectionModel.java:187)
	at javax.swing.JTree.setSelectionPath(JTree.java:1631)
	at javax.swing.plaf.basic.BasicTreeUI.selectPathForEvent(BasicTreeUI.java:2393)
	at javax.swing.plaf.basic.BasicTreeUI$Handler.handleSelection(BasicTreeUI.java:3609)
	at javax.swing.plaf.basic.BasicTreeUI$Handler.mousePressed(BasicTreeUI.java:3548)
	at java.awt.Component.processMouseEvent(Component.java:6513)
	at javax.swing.JComponent.processMouseEvent(JComponent.java:3321)
	at java.awt.Component.processEvent(Component.java:6281)
	at java.awt.Container.processEvent(Container.java:2229)
	at java.awt.Component.dispatchEventImpl(Component.java:4872)
	at java.awt.Container.dispatchEventImpl(Container.java:2287)
	at java.awt.Component.dispatchEvent(Component.java:4698)
	at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4832)
	at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4489)
	at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4422)
	at java.awt.Container.dispatchEventImpl(Container.java:2273)
	at java.awt.Window.dispatchEventImpl(Window.java:2719)
	at java.awt.Component.dispatchEvent(Component.java:4698)
	at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:747)
	at java.awt.EventQueue.access$300(EventQueue.java:103)
	at java.awt.EventQueue$3.run(EventQueue.java:706)
	at java.awt.EventQueue$3.run(EventQueue.java:704)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:87)
	at java.awt.EventQueue$4.run(EventQueue.java:720)
	at java.awt.EventQueue$4.run(EventQueue.java:718)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
	at java.awt.EventQueue.dispatchEvent(EventQueue.java:717)
	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:242)
	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:161)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:150)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:146)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:138)
	at java.awt.EventDispatchThread.run(EventDispatchThread.java:91)

That's because some AST values are not valid.

The values can be examined inside "Groovy AST Viewer":

MethodNode - run

  • lineNumber: 1
  • lastLineNumber: 1
  • columnNumber: 21
  • lastColumnNumber: 10
  • start: 20
  • end: 20
  • code: BlockStatement
    • lineNumber: 1
    • lastLineNumber: 1
    • columnNumber: 21
    • lastColumnNumber: 10
    • start: 20
    • end: 20
@eric-milles
Copy link
Member

I can see the offsets in Eclipse as you indicate. However, I was not able to reproduce the exception. Can you have a look at my screen and let me know if I selected the right thing?

Is it a common case to try and run an empty script in the Groovy Console? If you had at least one statement, do you get the same error?

issue252

@eric-milles
Copy link
Member

The source positions are set by AntParserPlugin.fixModuleNodeLocations:

    // GRECLIPSE add
    private void fixModuleNodeLocations() {
        output.setStart(0);
        output.setEnd(locations.getEnd());
        output.setLineNumber(1);
        output.setColumnNumber(1);
        output.setLastColumnNumber(locations.getEndColumn());
        output.setLastLineNumber(locations.getEndLine());

        // only occurs if in a script
        // start location should be the start of either the first method or statement
        // end location should be the end of either the last method or statement
        BlockStatement statements = output.getStatementBlock();
        List<MethodNode> methods = output.getMethods();
        if (hasScriptMethodsOrStatements(statements, methods)) {
            ASTNode first = getFirst(statements, methods);
            ASTNode last = getLast(statements, methods);
            if (hasScriptStatements(statements)) {
                statements.setStart(first.getStart());
                statements.setLineNumber(first.getLineNumber());
                statements.setColumnNumber(first.getColumnNumber());
                statements.setEnd(last.getEnd());
                statements.setLastLineNumber(last.getLastLineNumber());
                statements.setLastColumnNumber(last.getLastColumnNumber());
            }
            if (output.getClasses().size() > 0) {
                ClassNode scriptClass = output.getClasses().get(0);
                scriptClass.setStart(first.getStart());
                scriptClass.setLineNumber(first.getLineNumber());
                scriptClass.setColumnNumber(first.getColumnNumber());
                scriptClass.setEnd(last.getEnd());
                scriptClass.setLastLineNumber(last.getLastLineNumber());
                scriptClass.setLastColumnNumber(last.getLastColumnNumber());

                // fix the run method to contain the start and end locations of the statement block
                MethodNode runMethod = scriptClass.getDeclaredMethod("run", new Parameter[0]);
                runMethod.setStart(first.getStart());
                runMethod.setLineNumber(first.getLineNumber());
                runMethod.setColumnNumber(first.getColumnNumber());
                runMethod.setEnd(last.getEnd());
                runMethod.setLastLineNumber(last.getLastLineNumber());
                runMethod.setLastColumnNumber(last.getLastColumnNumber());
            }
        }
    }

@darxriggs
Copy link
Author

Hi Eric, thanks for the fast feedback.

I used the wording "empty script" because Groovy parses such files as scripts.

In real we are talking about package-info.groovy files. They may only contain the package and optionally Javadoc and annotations (like this one).

For non-empty script files like this one:

package org.example

print 'me'

...the AST contains correct values in Eclipse:

MethodNode - run

  • lineNumber: 3
  • lastLineNumber: 3
  • columnNumber: 1
  • lastColumnNumber: 11
  • code: BlockStatement
    • lineNumber: 3
    • lastLineNumber: 3
    • columnNumber: 1
    • lastColumnNumber: 11

I came across the issue with invalid line & column numbers while scanning Groovy files with CodeNarc inside Eclipse. CodeNarc uses a lot of AST visitors and accesses source code via the line & column numbers. When they are not correct I have seen java.lang.StringIndexOutOfBoundsExceptions. So I am interested in a correct AST inside Eclipse. The Exception in Groovy Console is just a side effect of the wrong AST.

Looking at your screenshot I can confirm that you selected the MethodNode I was talking about. Just the -1 values shouldn't be there. Was this Groovy Console launched from the commandline?

You also said that you can see the offsets in Eclipse. Did you in mean in the "Groovy AST Viewer"? Because these values are not visible in the screenshot. So what are the values inside the AST viewer?

@eric-milles
Copy link
Member

@darxriggs Yes, I did launch the Groovy Console from Eclipse. I tried to follow your directions as closely as possible. And, yes the offsets in the AST Viewer match what you describe in the issue.

Can you say what may be throwing CodeNarc off by having source positions on the run method node? I'm not sure why it has been given positions, but that is one of the patches made by Greclipse. It may be possible to skip setting the positions in case there are no statements besides the package statement.

@eric-milles
Copy link
Member

If I change hasScriptStatements() to

    private boolean hasScriptStatements(BlockStatement statements) {
        return statements != null && statements.getStatements() != null &&
            !statements.getStatements().isEmpty() && statements.getStatements().get(0).getEnd() > 0;
    }

from

    private boolean hasScriptStatements(BlockStatement statements) {
        return statements != null && statements.getStatements() != null && statements.getStatements().size() > 0;
    }

I think the desired effect is achieved.

@eric-milles
Copy link
Member

There are cases of files with just package statement, imports and import annotations that are failing with this change. I'll have to look into what position information they require.

@darxriggs
Copy link
Author

CodeNarc accesses exact positions on the source code based on the line & column information from the AST - see for example SpaceAfterOpeningBraceRule::visitMethodEx(). That's also the rule currently failing for package-only files.

If you like you can reproduce the StringIndexOutOfBoundsException with this code in Eclipse:

@Grab(group='org.codenarc', module='CodeNarc', version='0.22')
import org.codenarc.analyzer.StringSourceAnalyzer
import org.codenarc.rule.formatting.SpaceAfterOpeningBraceRule
import org.codenarc.ruleset.CompositeRuleSet

def ruleSet = new CompositeRuleSet()
ruleSet.addRule(new SpaceAfterOpeningBraceRule())
def source = 'package org.example'
def results = new StringSourceAnalyzer(source).analyze(ruleSet)
print(results.violations)

It results in this stacktrace:

java.lang.StringIndexOutOfBoundsException: String index out of range: 20
	at org.codenarc.rule.formatting.SpaceAfterOpeningBraceAstVisitor.visitMethodEx(SpaceAfterOpeningBraceRule.groovy:99)
	at org.codenarc.rule.AbstractAstVisitor.visitMethod(AbstractAstVisitor.java:163)
	at org.codenarc.rule.AbstractAstVisitor.visitClass(AbstractAstVisitor.java:148)
	at org.codenarc.rule.AbstractAstVisitorRule.applyTo(AbstractAstVisitorRule.java:97)
	at org.codenarc.rule.AbstractRule.applyTo(AbstractRule.java:142)
	at org.codenarc.rule.Rule$applyTo.call(Unknown Source)
	at org.codenarc.analyzer.AbstractSourceAnalyzer.collectViolations(AbstractSourceAnalyzer.groovy:40)
	at org.codenarc.analyzer.StringSourceAnalyzer.analyze(StringSourceAnalyzer.groovy:36)
	at org.codenarc.analyzer.SourceAnalyzer$analyze.call(Unknown Source)
	at SpaceAfterOpeningBraceRuleIssueTest.run(SpaceAfterOpeningBraceRuleIssueTest.groovy:8)

Sure the CodeNarc rules (about 400) could be implemented in a more safer way by always double checking the line & column values against the source code prior to accessing values in the source code. But this would clutter code a lot and would only workaround this AST issue opened here. Other tools using the AST values would still be affected.

So my goal was the root cause to be fixed.

@eric-milles
Copy link
Member

I'm trying to see if I can fix the positions. I just need a bit more time to look into the tested cases of files with just imports and the behavior of add/organize imports.

@eric-milles
Copy link
Member

From another angle, could it be the presence of position info on the module, block statement and run method nodes is not the problem? Is the problem that the end offset is 1 past the length of the source?

@eric-milles
Copy link
Member

Here is where the +1 source position comes from:

    // creates a synthetic, empty statement that starts after the last import or package statement
    private Statement createSyntheticAfterImports() {
        ASTNode target = null;
        Statement synthetic = ReturnStatement.RETURN_NULL_OR_VOID;
        if (output.getImports() != null && !output.getImports().isEmpty()) {
            target = output.getImports().get(output.getImports().size() - 1);
        } else if (output.hasPackage()) {
            target = output.getPackage();
        }
        if (target != null) {
            synthetic = new ReturnStatement(ConstantExpression.NULL);
            synthetic.setStart(target.getEnd() + 1);
            synthetic.setEnd(target.getEnd() + 1);
            synthetic.setLineNumber(target.getLastLineNumber());
            synthetic.setLastLineNumber(target.getLineNumber());
            synthetic.setColumnNumber(target.getLastColumnNumber() + 1);
            synthetic.setLastColumnNumber(target.getColumnNumber() + 1);
        }
        return synthetic;
    }

This is also in AntParserPlugin as part of the Greclipse additions. Let me experiment with removing the +1s.

@darxriggs
Copy link
Author

I think the "start" and "end" offsets are added via Groovy-Eclipse. They are not available when groovyConsole is run from the commandline - see "Details" section in this issue. Therefore they are not the problem for CodeNarc.

@eric-milles
Copy link
Member

The snippet that I included above is where you are getting an index past the end of the file.

@eric-milles
Copy link
Member

Can you retest with the latest snapshot? Here is a direct link if the update site did not get changed due to unrelated test failure: https://build.spring.io/browse/GRECLIPSE-GE46-178/artifact/JOB1/Zipped-Update-Site/org.codehaus.groovy-2.9.2.xx-201701042322-e46-updatesite.zip

@darxriggs
Copy link
Author

I retested with Groovy-Eclipse 2.9.2.xx-201701080425-e45.

With this version "Groovy Console" and "SpaceAfterOpeningBraceRule" CodeNarc rule both work fine inside Eclipse.

The AST values are now as follows:

  • lineNumber: 1
  • lastLineNumber: 1
  • columnNumber: 19
  • lastColumnNumber: 19
  • start: 18
  • end: 18
  • code: BlockStatement
    • lineNumber: 1
    • lastLineNumber: 1
    • columnNumber: 19
    • lastColumnNumber: 19
    • start: 18
    • end: 18

Well done.

If possible the value should be -1 to make Groovy-Eclipse as similar as the real Groovy compiler. With the -1 value it's also possible to detect methods that are not present in the source code (generated methods).

@eric-milles
Copy link
Member

The position info is set on the run method and its return statement to enable adding imports and some other editing activities to a nearly empty source file.

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