ruby/src/org/netbeans/modules/ruby/RubyParser.java
author enebo@netbeans.org
Sun, 08 Dec 2013 11:39:16 -0600
changeset 4554 07958c1ff402
parent 4542 be97a5e85907
permissions -rw-r--r--
Too much stuff in one commit. Rename blocks now follows language semantics. Removal of more ASTPath consumers
tor@1
     1
/*
phrebejk@559
     2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
tor@1
     3
 *
jglick@4117
     4
 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
jglick@4117
     5
 *
jglick@4117
     6
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
jglick@4117
     7
 * Other names may be trademarks of their respective owners.
tor@1
     8
 *
phrebejk@559
     9
 * The contents of this file are subject to the terms of either the GNU
phrebejk@559
    10
 * General Public License Version 2 only ("GPL") or the Common
phrebejk@559
    11
 * Development and Distribution License("CDDL") (collectively, the
phrebejk@559
    12
 * "License"). You may not use this file except in compliance with the
phrebejk@559
    13
 * License. You can obtain a copy of the License at
phrebejk@559
    14
 * http://www.netbeans.org/cddl-gplv2.html
phrebejk@559
    15
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
phrebejk@559
    16
 * specific language governing permissions and limitations under the
phrebejk@559
    17
 * License.  When distributing the software, include this License Header
phrebejk@559
    18
 * Notice in each file and include the License file at
jglick@4117
    19
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
phrebejk@559
    20
 * particular file as subject to the "Classpath" exception as provided
jglick@4117
    21
 * by Oracle in the GPL Version 2 section of the License file that
phrebejk@559
    22
 * accompanied this code. If applicable, add the following below the
phrebejk@559
    23
 * License Header, with the fields enclosed by brackets [] replaced by
phrebejk@559
    24
 * your own identifying information:
tor@1
    25
 * "Portions Copyrighted [year] [name of copyright owner]"
tor@1
    26
 *
phrebejk@559
    27
 * Contributor(s):
phrebejk@559
    28
 *
tor@1
    29
 * The Original Software is NetBeans. The Initial Developer of the Original
tor@1
    30
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
tor@1
    31
 * Microsystems, Inc. All Rights Reserved.
phrebejk@559
    32
 *
phrebejk@559
    33
 * If you wish your version of this file to be governed by only the CDDL
phrebejk@559
    34
 * or only the GPL Version 2, indicate your decision by adding
phrebejk@559
    35
 * "[Contributor] elects to include this software in this distribution
phrebejk@559
    36
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
phrebejk@559
    37
 * single choice of license, a recipient has the option to distribute
phrebejk@559
    38
 * your version of this file under either the CDDL, the GPL Version 2 or
phrebejk@559
    39
 * to extend the choice of license to its licensees as provided above.
phrebejk@559
    40
 * However, if you add GPL Version 2 code and therefore, elected the GPL
phrebejk@559
    41
 * Version 2 license, then the option applies only if the new code is
phrebejk@559
    42
 * made subject to such option by the copyright holder.
tor@1
    43
 */
tor@1
    44
package org.netbeans.modules.ruby;
tor@1
    45
enebo@4515
    46
import java.io.IOException;
emononen@3533
    47
import java.io.StringReader;
mkrauskopf@3030
    48
import java.util.ArrayList;
mkrauskopf@3030
    49
import java.util.Collection;
tor@1
    50
import java.util.Iterator;
tor@1
    51
import java.util.List;
mkrauskopf@3030
    52
import javax.swing.event.ChangeListener;
tor@1
    53
import javax.swing.text.BadLocationException;
emononen@3259
    54
import org.jrubyparser.ast.Node;
emononen@3259
    55
import org.jrubyparser.ast.RootNode;
emononen@3259
    56
import org.jrubyparser.IRubyWarnings;
emononen@3259
    57
import org.jrubyparser.IRubyWarnings.ID;
emononen@3259
    58
import org.jrubyparser.SourcePosition;
emononen@3259
    59
import org.jrubyparser.lexer.LexerSource;
emononen@3259
    60
import org.jrubyparser.lexer.SyntaxException;
emononen@3259
    61
import org.jrubyparser.parser.ParserConfiguration;
emononen@3259
    62
import org.jrubyparser.parser.ParserResult;
emononen@3259
    63
import org.jrubyparser.parser.Ruby18Parser;
emononen@3264
    64
import org.jrubyparser.parser.Ruby19Parser;
enebo@4542
    65
import org.jrubyparser.parser.Ruby20Parser;
emononen@3264
    66
import org.netbeans.api.project.FileOwnerQuery;
emononen@3264
    67
import org.netbeans.api.project.Project;
emononen@3264
    68
import org.netbeans.api.ruby.platform.RubyPlatform;
mkrauskopf@3030
    69
import org.netbeans.modules.csl.api.ElementHandle;
mkrauskopf@3030
    70
import org.netbeans.modules.csl.api.Error;
mkrauskopf@3030
    71
import org.netbeans.modules.csl.api.OffsetRange;
mkrauskopf@3030
    72
import org.netbeans.modules.csl.api.Severity;
mkrauskopf@3030
    73
import org.netbeans.modules.csl.spi.GsfUtilities;
mkrauskopf@3030
    74
import org.netbeans.modules.parsing.api.Snapshot;
mkrauskopf@3030
    75
import org.netbeans.modules.parsing.api.Task;
mkrauskopf@3030
    76
import org.netbeans.modules.parsing.spi.ParseException;
mkrauskopf@3030
    77
import org.netbeans.modules.parsing.spi.Parser;
mkrauskopf@3030
    78
import org.netbeans.modules.parsing.spi.ParserFactory;
mkrauskopf@3030
    79
import org.netbeans.modules.parsing.spi.SourceModificationEvent;
tor@1
    80
import org.netbeans.modules.ruby.elements.AstElement;
tor@1205
    81
import org.netbeans.modules.ruby.elements.RubyElement;
emononen@3264
    82
import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator;
tor@1291
    83
import org.openide.filesystems.FileObject;
tor@1
    84
import org.openide.util.NbBundle;
tor@1
    85
tor@1
    86
tor@1
    87
/**
tor@1
    88
 * Wrapper around JRuby to parse a buffer into an AST.
tor@1
    89
 *
tor@1
    90
 * @todo Rename to RubyParser for symmetry with RubyLexer
tor@1
    91
 * @todo Idea: If you get a syntax error on the last line, it's probably a missing
tor@1
    92
 *   "end" much earlier. Go back and look for a method inside a method, and the outer
tor@1
    93
 *   method is probably missing an end (can use indentation to look for this as well).
tor@1
    94
 *   Create a quickfix to insert it.
tor@468
    95
 * @todo Only look for missing-end if there's an unexpected end
tor@468
    96
 * @todo If you get a "class definition in method body" error, there's a missing
tor@468
    97
 *   end - prior to the class!
tor@468
    98
 * @todo "syntax error, unexpected tRCURLY" means that I also have a missing end,
tor@468
    99
 *   but we encountered a } before we got to it. I need to be bracketing this stuff.
tor@371
   100
 * 
tor@1
   101
 * @author Tor Norbye
tor@1
   102
 */
mkrauskopf@3030
   103
public final class RubyParser extends Parser {
mkrauskopf@3030
   104
emononen@4184
   105
    /**
enebo@4515
   106
     * System property for defaulting to 1.8 parser.
emononen@4184
   107
     */
enebo@4515
   108
    private static boolean DEFAULT_TO_RUBY18 = Boolean.getBoolean("ruby.parser.default18"); //NOI18N
emononen@4184
   109
mkrauskopf@3030
   110
    private RubyParseResult lastResult;
tor@1
   111
tor@1
   112
    /**
tor@1
   113
     * Creates a new instance of RubyParser
tor@1
   114
     */
tor@1
   115
    public RubyParser() {
tor@1
   116
    }
tor@1
   117
mkrauskopf@3030
   118
    // ------------------------------------------------------------------------
mkrauskopf@3030
   119
    // o.n.m.p.spi.Parser implementation
mkrauskopf@3030
   120
    // ------------------------------------------------------------------------
mkrauskopf@3030
   121
mkrauskopf@3030
   122
    public @Override void parse(Snapshot snapshot, Task task, SourceModificationEvent event) throws ParseException {
mkrauskopf@3030
   123
        Context context = new Context(snapshot, event);
mkrauskopf@3030
   124
        final List<Error> errors = new ArrayList<Error>();
mkrauskopf@3030
   125
        context.errorHandler = new ParseErrorHandler() {
enebo@4515
   126
            @Override
mkrauskopf@3030
   127
            public void error(Error error) {
mkrauskopf@3030
   128
                errors.add(error);
mkrauskopf@3030
   129
            }
mkrauskopf@3030
   130
        };
mkrauskopf@3030
   131
        lastResult = parseBuffer(context, Sanitize.NONE);
mkrauskopf@3030
   132
        lastResult.setErrors(errors);
mkrauskopf@3030
   133
    }
mkrauskopf@3030
   134
mkrauskopf@3030
   135
    public @Override Result getResult(Task task) throws ParseException {
mkrauskopf@3030
   136
        assert lastResult != null : "getResult() called prior parse()"; //NOI18N
mkrauskopf@3030
   137
        return lastResult;
mkrauskopf@3030
   138
    }
mkrauskopf@3030
   139
mkrauskopf@3030
   140
    public @Override void addChangeListener(ChangeListener changeListener) {
mkrauskopf@3030
   141
        // no-op, we don't support state changes
mkrauskopf@3030
   142
    }
mkrauskopf@3030
   143
mkrauskopf@3030
   144
    public @Override void removeChangeListener(ChangeListener changeListener) {
mkrauskopf@3030
   145
        // no-op, we don't support state changes
mkrauskopf@3030
   146
    }
mkrauskopf@3030
   147
mkrauskopf@3030
   148
mkrauskopf@3030
   149
    // ------------------------------------------------------------------------
mkrauskopf@3030
   150
    // o.n.m.p.spi.ParserFactory implementation
mkrauskopf@3030
   151
    // ------------------------------------------------------------------------
mkrauskopf@3030
   152
mkrauskopf@3030
   153
    private static final class Factory extends ParserFactory {
mkrauskopf@3030
   154
mkrauskopf@3030
   155
        @Override
mkrauskopf@3030
   156
        public Parser createParser(Collection<Snapshot> snapshots) {
mkrauskopf@3030
   157
            return new RubyParser();
mkrauskopf@3030
   158
        }
mkrauskopf@3030
   159
mkrauskopf@3030
   160
    } // End of Factory class
mkrauskopf@3030
   161
mkrauskopf@3030
   162
tor@1
   163
    private static String asString(CharSequence sequence) {
tor@1
   164
        if (sequence instanceof String) {
tor@1
   165
            return (String)sequence;
tor@1
   166
        } else {
tor@1
   167
            return sequence.toString();
tor@1
   168
        }
tor@1
   169
    }
tor@1
   170
tor@1
   171
    /**
tor@1
   172
     * Try cleaning up the source buffer around the current offset to increase
tor@1
   173
     * likelihood of parse success. Initially this method had a lot of
tor@1
   174
     * logic to determine whether a parse was likely to fail (e.g. invoking
tor@1
   175
     * the isEndMissing method from bracket completion etc.).
tor@1
   176
     * However, I am now trying a parse with the real source first, and then
tor@1
   177
     * only if that fails do I try parsing with sanitized source. Therefore,
tor@1
   178
     * this method has to be less conservative in ripping out code since it
tor@1
   179
     * will only be used when the regular source is failing.
tor@1
   180
     */
tor@364
   181
    private boolean sanitizeSource(Context context, Sanitize sanitizing) {
tor@364
   182
tor@364
   183
        if (sanitizing == Sanitize.MISSING_END) {
tor@364
   184
            context.sanitizedSource = context.source + ";end";
tor@364
   185
            int start = context.source.length();
tor@364
   186
            context.sanitizedRange = new OffsetRange(start, start+4);
tor@581
   187
            context.sanitizedContents = "";
tor@364
   188
            return true;
tor@364
   189
        }
tor@364
   190
tor@364
   191
        int offset = context.caretOffset;
tor@364
   192
tor@1
   193
        // Let caretOffset represent the offset of the portion of the buffer we'll be operating on
tor@1
   194
        if ((sanitizing == Sanitize.ERROR_DOT) || (sanitizing == Sanitize.ERROR_LINE)) {
tor@364
   195
            offset = context.errorOffset;
tor@1
   196
        }
tor@1
   197
tor@1
   198
        // Don't attempt cleaning up the source if we don't have the buffer position we need
tor@364
   199
        if (offset == -1) {
tor@364
   200
            return false;
tor@1
   201
        }
tor@1
   202
tor@1
   203
        // The user might be editing around the given caretOffset.
tor@1
   204
        // See if it looks modified
tor@1
   205
        // Insert an end statement? Insert a } marker?
tor@478
   206
        String doc = context.source;
tor@478
   207
        if (offset > doc.length()) {
tor@364
   208
            return false;
tor@1
   209
        }
tor@1
   210
tor@797
   211
        if (sanitizing == Sanitize.BLOCK_START) {
tor@797
   212
            try {
tor@2469
   213
                int start = GsfUtilities.getRowFirstNonWhite(doc, offset);
tor@797
   214
                if (start != -1 && 
tor@797
   215
                        start+2 < doc.length() &&
tor@797
   216
                        doc.regionMatches(start, "if", 0, 2)) {
tor@797
   217
                    // TODO - check lexer
tor@797
   218
                    char c = 0;
tor@797
   219
                    if (start+2 < doc.length()) {
tor@797
   220
                        c = doc.charAt(start+2);
tor@797
   221
                    }
tor@797
   222
                    if (!Character.isLetter(c)) {
tor@797
   223
                        int removeStart = start;
tor@797
   224
                        int removeEnd = removeStart+2;
tor@797
   225
                        StringBuilder sb = new StringBuilder(doc.length());
tor@797
   226
                        sb.append(doc.substring(0, removeStart));
tor@797
   227
                        for (int i = removeStart; i < removeEnd; i++) {
tor@797
   228
                            sb.append(' ');
tor@797
   229
                        }
tor@797
   230
                        if (removeEnd < doc.length()) {
tor@797
   231
                            sb.append(doc.substring(removeEnd, doc.length()));
tor@797
   232
                        }
tor@797
   233
                        assert sb.length() == doc.length();
tor@797
   234
                        context.sanitizedRange = new OffsetRange(removeStart, removeEnd);
tor@797
   235
                        context.sanitizedSource = sb.toString();
tor@797
   236
                        context.sanitizedContents = doc.substring(removeStart, removeEnd);
tor@797
   237
                        return true;
tor@797
   238
                    }
tor@797
   239
                }
tor@797
   240
                
tor@797
   241
                return false;
tor@797
   242
            } catch (BadLocationException ble) {
tor@797
   243
                return false;
tor@797
   244
            }
tor@797
   245
        }
tor@797
   246
        
tor@1
   247
        try {
tor@1
   248
            // Sometimes the offset shows up on the next line
tor@2469
   249
            if (GsfUtilities.isRowEmpty(doc, offset) || GsfUtilities.isRowWhite(doc, offset)) {
tor@2469
   250
                offset = GsfUtilities.getRowStart(doc, offset)-1;
tor@364
   251
                if (offset < 0) {
tor@364
   252
                    offset = 0;
tor@1
   253
                }
tor@1
   254
            }
tor@1
   255
tor@2469
   256
            if (!(GsfUtilities.isRowEmpty(doc, offset) || GsfUtilities.isRowWhite(doc, offset))) {
tor@1
   257
                if ((sanitizing == Sanitize.EDITED_LINE) || (sanitizing == Sanitize.ERROR_LINE)) {
tor@1
   258
                    // See if I should try to remove the current line, since it has text on it.
tor@2469
   259
                    int lineEnd = GsfUtilities.getRowLastNonWhite(doc, offset);
tor@1
   260
tor@1
   261
                    if (lineEnd != -1) {
tor@478
   262
                        StringBuilder sb = new StringBuilder(doc.length());
tor@2469
   263
                        int lineStart = GsfUtilities.getRowStart(doc, offset);
tor@1
   264
                        int rest = lineStart + 1;
tor@1
   265
tor@478
   266
                        sb.append(doc.substring(0, lineStart));
tor@1
   267
                        sb.append('#');
tor@1
   268
tor@478
   269
                        if (rest < doc.length()) {
tor@478
   270
                            sb.append(doc.substring(rest, doc.length()));
tor@1
   271
                        }
tor@478
   272
                        assert sb.length() == doc.length();
tor@1
   273
tor@364
   274
                        context.sanitizedRange = new OffsetRange(lineStart, lineEnd);
tor@364
   275
                        context.sanitizedSource = sb.toString();
tor@581
   276
                        context.sanitizedContents = doc.substring(lineStart, lineEnd);
tor@364
   277
                        return true;
tor@1
   278
                    }
tor@1
   279
                } else {
tor@364
   280
                    assert sanitizing == Sanitize.ERROR_DOT || sanitizing == Sanitize.EDITED_DOT;
tor@1
   281
                    // Try nuking dots/colons from this line
tor@1
   282
                    // See if I should try to remove the current line, since it has text on it.
tor@2469
   283
                    int lineStart = GsfUtilities.getRowStart(doc, offset);
tor@581
   284
                    int lineEnd = offset-1;
tor@581
   285
                    while (lineEnd >= lineStart && lineEnd < doc.length()) {
tor@581
   286
                        if (!Character.isWhitespace(doc.charAt(lineEnd))) {
tor@581
   287
                            break;
tor@581
   288
                        }
tor@581
   289
                        lineEnd--;
tor@581
   290
                    }
tor@581
   291
                    if (lineEnd > lineStart) {
tor@478
   292
                        StringBuilder sb = new StringBuilder(doc.length());
tor@478
   293
                        String line = doc.substring(lineStart, lineEnd + 1);
tor@1
   294
                        int removeChars = 0;
tor@581
   295
                        int removeEnd = lineEnd+1;
tor@1
   296
tor@478
   297
                        if (line.endsWith(".") || line.endsWith("(")) { // NOI18N
tor@1
   298
                            removeChars = 1;
tor@581
   299
                        } else if (line.endsWith(",")) { // NOI18N                            removeChars = 1;
tor@581
   300
                            removeChars = 1;
tor@581
   301
                        } else if (line.endsWith(",:")) { // NOI18N
tor@581
   302
                            removeChars = 2;
tor@581
   303
                        } else if (line.endsWith(", :")) { // NOI18N
tor@581
   304
                            removeChars = 3;
tor@581
   305
                        } else if (line.endsWith(", ")) { // NOI18N
tor@581
   306
                            removeChars = 2;
tor@581
   307
                        } else if (line.endsWith("=> :")) { // NOI18N
tor@581
   308
                            removeChars = 4;
tor@581
   309
                        } else if (line.endsWith("=>:")) { // NOI18N
tor@581
   310
                            removeChars = 3;
tor@581
   311
                        } else if (line.endsWith("=>")) { // NOI18N
tor@581
   312
                            removeChars = 2;
tor@478
   313
                        } else if (line.endsWith("::")) { // NOI18N
tor@1
   314
                            removeChars = 2;
tor@544
   315
                        } else if (line.endsWith(":")) { // NOI18N
tor@544
   316
                            removeChars = 1;
tor@478
   317
                        } else if (line.endsWith("@@")) { // NOI18N
tor@478
   318
                            removeChars = 2;
tor@2324
   319
                        } else if (line.endsWith("@") || line.endsWith("$")) { // NOI18N
tor@478
   320
                            removeChars = 1;
tor@478
   321
                        } else if (line.endsWith(",)")) { // NOI18N
tor@1
   322
                            // Handle lone comma in parameter list - e.g.
tor@1
   323
                            // type "foo(a," -> you end up with "foo(a,|)" which doesn't parse - but
tor@1
   324
                            // the line ends with ")", not "," !
tor@1
   325
                            // Just remove the comma
tor@1
   326
                            removeChars = 1;
tor@581
   327
                            removeEnd--;
tor@478
   328
                        } else if (line.endsWith(", )")) { // NOI18N
tor@1
   329
                            // Just remove the comma
tor@1
   330
                            removeChars = 1;
tor@581
   331
                            removeEnd -= 2;
tor@1
   332
                        }
tor@364
   333
                        
tor@364
   334
                        if (removeChars == 0) {
tor@364
   335
                            return false;
tor@364
   336
                        }
tor@1
   337
tor@581
   338
                        int removeStart = removeEnd-removeChars;
tor@1
   339
tor@581
   340
                        sb.append(doc.substring(0, removeStart));
tor@1
   341
tor@1
   342
                        for (int i = 0; i < removeChars; i++) {
tor@1
   343
                            sb.append(' ');
tor@1
   344
                        }
tor@1
   345
tor@581
   346
                        if (removeEnd < doc.length()) {
tor@581
   347
                            sb.append(doc.substring(removeEnd, doc.length()));
tor@1
   348
                        }
tor@478
   349
                        assert sb.length() == doc.length();
tor@1
   350
tor@581
   351
                        context.sanitizedRange = new OffsetRange(removeStart, removeEnd);
tor@364
   352
                        context.sanitizedSource = sb.toString();
tor@581
   353
                        context.sanitizedContents = doc.substring(removeStart, removeEnd);
tor@364
   354
                        return true;
tor@1
   355
                    }
tor@1
   356
                }
tor@1
   357
            }
tor@1
   358
        } catch (BadLocationException ble) {
emononen@3695
   359
            // do nothing - see #154991
tor@1
   360
        }
tor@1
   361
tor@364
   362
        return false;
tor@1
   363
    }
tor@478
   364
    
tor@364
   365
    @SuppressWarnings("fallthrough")
mkrauskopf@3030
   366
    private RubyParseResult sanitize(final Context context, final Sanitize sanitizing) {
tor@364
   367
tor@364
   368
        switch (sanitizing) {
tor@364
   369
        case NEVER:
mkrauskopf@3030
   370
            return createParseResult(context.snapshot, null);
tor@364
   371
tor@364
   372
        case NONE:
tor@364
   373
tor@364
   374
            // We've currently tried with no sanitization: try first level
tor@364
   375
            // of sanitization - removing dots/colons at the edited offset.
tor@364
   376
            // First try removing the dots or double colons around the failing position
tor@364
   377
            if (context.caretOffset != -1) {
tor@364
   378
                return parseBuffer(context, Sanitize.EDITED_DOT);
tor@364
   379
            }
tor@364
   380
tor@364
   381
        // Fall through to try the next trick
tor@364
   382
        case EDITED_DOT:
tor@364
   383
tor@364
   384
            // We've tried editing the caret location - now try editing the error location
tor@478
   385
            // (Don't bother doing this if errorOffset==caretOffset since that would try the same
tor@478
   386
            // source as EDITED_DOT which has no better chance of succeeding...)
tor@478
   387
            if (context.errorOffset != -1 && context.errorOffset != context.caretOffset) {
tor@364
   388
                return parseBuffer(context, Sanitize.ERROR_DOT);
tor@364
   389
            }
tor@364
   390
tor@364
   391
        // Fall through to try the next trick
tor@364
   392
        case ERROR_DOT:
tor@364
   393
tor@364
   394
            // We've tried removing dots - now try removing the whole line at the error position
tor@797
   395
            if (context.caretOffset != -1) {
tor@797
   396
                return parseBuffer(context, Sanitize.BLOCK_START);
tor@797
   397
            }
tor@797
   398
            
tor@797
   399
        // Fall through to try the next trick
tor@797
   400
        case BLOCK_START:
tor@797
   401
            
tor@797
   402
            // We've tried removing dots - now try removing the whole line at the error position
tor@364
   403
            if (context.errorOffset != -1) {
tor@364
   404
                return parseBuffer(context, Sanitize.ERROR_LINE);
tor@364
   405
            }
tor@364
   406
tor@364
   407
        // Fall through to try the next trick
tor@364
   408
        case ERROR_LINE:
tor@364
   409
tor@364
   410
            // Messing with the error line didn't work - we could try "around" the error line
tor@364
   411
            // but I'm not attempting that now.
tor@364
   412
            // Finally try removing the whole line around the user editing position
tor@364
   413
            // (which could be far from where the error is showing up - but if you're typing
tor@364
   414
            // say a new "def" statement in a class, this will show up as an error on a mismatched
tor@364
   415
            // "end" statement rather than here
tor@364
   416
            if (context.caretOffset != -1) {
tor@364
   417
                return parseBuffer(context, Sanitize.EDITED_LINE);
tor@364
   418
            }
tor@364
   419
tor@364
   420
        // Fall through to try the next trick
tor@364
   421
        case EDITED_LINE:
tor@364
   422
            return parseBuffer(context, Sanitize.MISSING_END);
tor@364
   423
            
tor@364
   424
        // Fall through for default handling
tor@364
   425
        case MISSING_END:
tor@364
   426
        default:
tor@364
   427
            // We're out of tricks - just return the failed parse result
mkrauskopf@3030
   428
            return createParseResult(context.snapshot, null);
tor@364
   429
        }
tor@364
   430
    }
tor@1291
   431
    
tor@1291
   432
    protected void notifyError(Context context, ID id,
tor@1291
   433
        Severity severity, String description, int offset, Sanitize sanitizing, Object[] data) {
enebo@4529
   434
        if (description == null) description = "";
enebo@4518
   435
        if (description.startsWith("syntax error, ")) description = description.substring(14);
enebo@4518
   436
tor@2501
   437
        if (description.startsWith("unexpected k")) {
tor@2501
   438
            description = "Unexpected keyword " + description.substring(12);
tor@2501
   439
        }
tor@1
   440
        // Replace a common but unwieldy JRuby error message with a shorter one
tor@1
   441
        if (description.startsWith("syntax error, expecting	")) { // NOI18N
tor@1
   442
            int start = description.indexOf(" but found "); // NOI18N
tor@1
   443
            assert start != -1;
tor@1
   444
            start += 11;
tor@1
   445
            int end = description.indexOf("instead", start); // NOI18N
tor@1
   446
            assert end != -1;
tor@1
   447
            String found = description.substring(start, end);
tor@1291
   448
            description = NbBundle.getMessage(RubyParser.class, "UnexpectedError", found);
tor@1
   449
        }
tor@2501
   450
tor@2501
   451
        if (description.length() > 0) {
tor@2501
   452
            // Capitalize sentences
tor@2501
   453
            char firstChar = description.charAt(0);
tor@2501
   454
            char upcasedChar = Character.toUpperCase(firstChar);
tor@2501
   455
            if (firstChar != upcasedChar) {
tor@2501
   456
                description = upcasedChar + description.substring(1);
tor@2501
   457
            }
tor@2501
   458
        }
tor@1
   459
        
mkrauskopf@3030
   460
        Error error = new RubyError(description, id, context.snapshot.getSource().getFileObject(), offset, offset, severity, data);
emononen@3102
   461
        context.errorHandler.error(error);
tor@1
   462
        if (sanitizing == Sanitize.NONE) {
tor@364
   463
            context.errorOffset = offset;
tor@1
   464
        }
tor@1
   465
    }
tor@1
   466
tor@544
   467
    protected RubyParseResult parseBuffer(final Context context, final Sanitize sanitizing) {
tor@1
   468
        boolean sanitizedSource = false;
tor@364
   469
        String source = context.source;
tor@364
   470
        if (!((sanitizing == Sanitize.NONE) || (sanitizing == Sanitize.NEVER))) {
tor@364
   471
            boolean ok = sanitizeSource(context, sanitizing);
tor@1
   472
tor@364
   473
            if (ok) {
tor@364
   474
                assert context.sanitizedSource != null;
tor@1
   475
                sanitizedSource = true;
tor@364
   476
                source = context.sanitizedSource;
tor@364
   477
            } else {
tor@364
   478
                // Try next trick
tor@364
   479
                return sanitize(context, sanitizing);
tor@1
   480
            }
tor@1
   481
        }
tor@1
   482
emononen@3259
   483
        ParserResult result = null;
tor@1
   484
tor@1
   485
        final boolean ignoreErrors = sanitizedSource;
tor@1
   486
tor@1
   487
        try {
tor@1
   488
            IRubyWarnings warnings =
tor@1
   489
                new IRubyWarnings() {
enebo@4515
   490
                    @Override
tor@1
   491
                    public boolean isVerbose() {
tor@1
   492
                        return false;
tor@1
   493
                    }
tor@1
   494
enebo@4515
   495
                    @Override
emononen@3259
   496
                    public void warn(ID id, SourcePosition position, String message, Object... data) {
tor@1
   497
                        if (!ignoreErrors) {
tor@1291
   498
                            notifyError(context, id, Severity.WARNING, message, position.getStartOffset(),
tor@1291
   499
                                sanitizing, data);
tor@1
   500
                        }
tor@1
   501
                    }
tor@1
   502
enebo@4515
   503
                    @Override
tor@1291
   504
                    public void warn(ID id, String fileName, int lineNumber, String message, Object... data) {
tor@1291
   505
                        // XXX What about a the position? Compute from fileName+lineNumber?
tor@1
   506
                        if (!ignoreErrors) {
tor@1291
   507
                            notifyError(context, id, Severity.WARNING, message, -1,
tor@1291
   508
                                sanitizing, data);
tor@1
   509
                        }
tor@1
   510
                    }
tor@1
   511
enebo@4515
   512
                    @Override
tor@1291
   513
                    public void warn(ID id, String message, Object... data) {
tor@1
   514
                        if (!ignoreErrors) {
tor@1291
   515
                            notifyError(context, id, Severity.WARNING, message, -1,
tor@1291
   516
                                sanitizing, data);
tor@1291
   517
                        }
tor@1291
   518
                    }
tor@1291
   519
enebo@4515
   520
                    @Override
tor@1291
   521
                    public void warning(ID id, String message, Object... data) {
tor@1291
   522
                        if (!ignoreErrors) {
tor@1291
   523
                            notifyError(context, id, Severity.WARNING, message, -1,
tor@1291
   524
                                sanitizing, data);
tor@1291
   525
                        }
tor@1291
   526
                    }
tor@1291
   527
enebo@4515
   528
                    @Override
emononen@3259
   529
                    public void warning(ID id, SourcePosition position, String message, Object... data) {
tor@1291
   530
                        if (!ignoreErrors) {
tor@1291
   531
                            notifyError(context, id, Severity.WARNING, message, position.getStartOffset(),
tor@1291
   532
                                sanitizing, data);
tor@1291
   533
                        }
tor@1291
   534
                    }
tor@1291
   535
enebo@4515
   536
                    @Override
tor@1291
   537
                    public void warning(ID id, String fileName, int lineNumber, String message, Object... data) {
tor@1291
   538
                        // XXX What about a the position? Compute from fileName+lineNumber?
tor@1291
   539
                        if (!ignoreErrors) {
tor@1291
   540
                            notifyError(context, id, Severity.WARNING, message, -1,
tor@1291
   541
                                sanitizing, data);
tor@1
   542
                        }
tor@1
   543
                    }
tor@1
   544
                };
tor@1
   545
tor@1
   546
            //warnings.setFile(file);
emononen@3264
   547
            org.jrubyparser.parser.RubyParser parser = getParserFor(context);
tor@1
   548
            parser.setWarnings(warnings);
tor@1
   549
tor@1
   550
            if (sanitizing == Sanitize.NONE) {
tor@364
   551
                context.errorOffset = -1;
tor@1
   552
            }
tor@1
   553
tor@1
   554
            String fileName = "";
tor@1
   555
mkrauskopf@3030
   556
            final FileObject fo = context.snapshot.getSource().getFileObject();
mkrauskopf@3030
   557
            if (fo != null) {
mkrauskopf@3030
   558
                fileName = fo.getNameExt();
tor@1
   559
            }
tor@1
   560
emononen@3259
   561
            ParserConfiguration configuration = new ParserConfiguration();
emononen@3533
   562
            LexerSource lexerSource =
emononen@3533
   563
                    LexerSource.getSource(fileName, new StringReader(source), configuration);
tor@1
   564
            result = parser.parse(configuration, lexerSource);
enebo@4515
   565
        } catch (IOException e) {
enebo@4515
   566
            // FIXME: This should do something other than ignore?
tor@1
   567
        } catch (SyntaxException e) {
tor@1
   568
            int offset = e.getPosition().getStartOffset();
tor@1
   569
tor@1
   570
            // XXX should this be >, and = length?
tor@1
   571
            if (offset >= source.length()) {
tor@1
   572
                offset = source.length() - 1;
tor@1
   573
tor@1
   574
                if (offset < 0) {
tor@1
   575
                    offset = 0;
tor@1
   576
                }
tor@1
   577
            }
tor@1
   578
tor@1
   579
            if (!ignoreErrors) {
emononen@3259
   580
                //XXX: jruby-parser
tor@1291
   581
                notifyError(context, ID.SYNTAX_ERROR, Severity.ERROR, e.getMessage(),
tor@1291
   582
                   offset, sanitizing, new Object[] { e.getPid(), e });
tor@1
   583
            }
tor@1
   584
        }
tor@1
   585
tor@1
   586
        Node root = (result != null) ? result.getAST() : null;
tor@1
   587
enebo@4542
   588
        // FIXME: Change all code to not mind searching from RootNode
tor@1
   589
        if (root instanceof RootNode) {
enebo@4542
   590
            Node body = ((RootNode) root).getBody();
enebo@4542
   591
            
enebo@4542
   592
            if (body != null) root = body;
tor@1
   593
        }
tor@1
   594
enebo@4542
   595
        // FIXME: Is can a non-null result really have a null root node?
tor@1
   596
        if (root != null) {
tor@364
   597
            context.sanitized = sanitizing;
mkrauskopf@3030
   598
            RubyParseResult r = createParseResult(context.snapshot, root);
tor@581
   599
            r.setSanitized(context.sanitized, context.sanitizedRange, context.sanitizedContents);
tor@1
   600
            r.setSource(source);
tor@1
   601
            return r;
tor@1
   602
        }
enebo@4542
   603
        
enebo@4542
   604
        return sanitize(context, sanitizing);
tor@1
   605
    }
emononen@3259
   606
emononen@3264
   607
emononen@3264
   608
    /**
emononen@3264
   609
     * Gets the parser for the given context. If the context is owned by 
emononen@3264
   610
     * a project that uses Ruby 1.9 or JRuby with 1.9 turned on, this method 
emononen@3264
   611
     * will return a 1.9 compatible parser; otherwise a 1.8 compatible parser. 
emononen@3264
   612
     * 
emononen@3264
   613
     * @param context
emononen@3264
   614
     * @return
emononen@3264
   615
     */
emononen@3264
   616
    private static org.jrubyparser.parser.RubyParser getParserFor(Context context) {
emononen@3264
   617
        // currently there is no way to specify a source level for the project 
emononen@3264
   618
        // using a UI. instead the source version is determined by the platform the project
emononen@3264
   619
        // uses, or in case JRuby that can support both 1.8 and 1.9 we check for the 
emononen@3264
   620
        // specified compat level
emononen@3264
   621
        FileObject fo = context.snapshot.getSource().getFileObject();
enebo@4515
   622
        if (fo == null) return getDefaultParser();
enebo@4515
   623
emononen@3264
   624
        Project owner = FileOwnerQuery.getOwner(fo);
enebo@4515
   625
        if (owner == null) return getDefaultParser();
enebo@4515
   626
emononen@3264
   627
        RubyPlatform platform = RubyPlatform.platformFor(owner);
enebo@4515
   628
enebo@4515
   629
        if (platform == null) return getDefaultParser();
enebo@4515
   630
        if (platform.isJRuby()) return getParserForJRuby(owner);
enebo@4542
   631
        
enebo@4542
   632
        return getParserFromProject(owner);
enebo@4542
   633
    }
enebo@4542
   634
    
enebo@4542
   635
    private static org.jrubyparser.parser.RubyParser getParserFromProject(Project owner) {
enebo@4542
   636
        RubyPlatform platform = RubyPlatform.platformFor(owner);
enebo@4542
   637
        
enebo@4542
   638
        if (platform != null) {
enebo@4542
   639
            if (platform.is18()) return new Ruby18Parser();
enebo@4542
   640
            if (platform.is19()) return new Ruby19Parser();
enebo@4542
   641
            if (platform.is20()) return new Ruby20Parser();
enebo@4542
   642
        }
enebo@4542
   643
        
enebo@4542
   644
        return getDefaultParser();        
enebo@4542
   645
    }
enebo@4542
   646
    
enebo@4542
   647
    private static org.jrubyparser.parser.RubyParser getDefaultParser() {
enebo@4542
   648
        return DEFAULT_TO_RUBY18 ? new Ruby18Parser() : new Ruby20Parser();
emononen@3264
   649
    }
emononen@3264
   650
emononen@3264
   651
    private static org.jrubyparser.parser.RubyParser getParserForJRuby(Project project) {
emononen@3264
   652
        PropertyEvaluator evaluator = project.getLookup().lookup(PropertyEvaluator.class);
emononen@3264
   653
        if (evaluator != null) {
emononen@3264
   654
            // specified in SharedRubyProjectProperties, but don't want add a dep to it.
emononen@3264
   655
            String jvmArgs = evaluator.getProperty("jvm.args"); //NOI18N
emononen@3264
   656
            if (jvmArgs != null) {
enebo@4542
   657
                if (jvmArgs.contains("jruby.compat.version=RUBY1_8")) return new Ruby18Parser();
enebo@4542
   658
                if (jvmArgs.contains("jruby.compat.version=RUBY1_9")) return new Ruby19Parser();
enebo@4542
   659
                if (jvmArgs.contains("jruby.compat.version=RUBY2_0")) return new Ruby20Parser();
emononen@3264
   660
            }
emononen@3264
   661
        }
enebo@4542
   662
        return getParserFromProject(project);
emononen@3264
   663
    }
emononen@3264
   664
mkrauskopf@3030
   665
    protected RubyParseResult createParseResult(Snapshot snapshots, Node rootNode) {
enebo@4554
   666
        return new RubyParseResult(snapshots, rootNode);
tor@544
   667
    }
tor@544
   668
    
emononen@3259
   669
    public static RubyElement resolveHandle(org.netbeans.modules.csl.spi.ParserResult info, ElementHandle handle) {
enebo@4542
   670
        if (handle instanceof RubyElement) return (RubyElement) handle;
tor@1205
   671
        if (handle instanceof AstElement) {
tor@1205
   672
            AstElement element = (AstElement)handle;
enebo@4542
   673
            
emononen@3259
   674
            org.netbeans.modules.csl.spi.ParserResult oldInfo = element.getInfo();
enebo@4542
   675
            if (oldInfo == info) return element;
enebo@4542
   676
tor@1238
   677
            Node oldNode = element.getNode();
tor@1205
   678
            Node oldRoot = AstUtilities.getRoot(oldInfo);
tor@1205
   679
            
tor@1205
   680
            Node newRoot = AstUtilities.getRoot(info);
enebo@4542
   681
            if (newRoot == null) return null;
tor@1
   682
tor@1205
   683
            // Find newNode
tor@1205
   684
            Node newNode = find(oldRoot, oldNode, newRoot);
enebo@4542
   685
            if (newNode != null) return AstElement.create(info, newNode);
tor@1
   686
        }
tor@1
   687
        return null;
tor@1
   688
    }
tor@1
   689
enebo@4542
   690
    /**
enebo@4542
   691
     * Assumes nearly or completely identical ASTs.  Finds same point in new tree where oldObject
enebo@4542
   692
     * was in old tree.
enebo@4542
   693
     */
tor@1198
   694
    private static Node find(Node oldRoot, Node oldObject, Node newRoot) {
tor@1
   695
        // Walk down the tree to locate oldObject, and in the process, pick the same child for newRoot
enebo@4542
   696
        Iterator<Node> itNew = newRoot.childNodes().iterator();
enebo@4542
   697
        
enebo@4542
   698
        for (Node o: oldRoot.childNodes()) {
enebo@4542
   699
            if (!itNew.hasNext()) return null; // different number of nodes between trees.  mismatch
enebo@4542
   700
            
enebo@4542
   701
            if (o == oldObject) return itNew.next(); // found location in old tree return new equiv.
enebo@4542
   702
            
enebo@4542
   703
            Node match = find(o, oldObject, itNew.next());  // depth-first search
enebo@4542
   704
            if (match != null) return match;
tor@1
   705
        }
enebo@4542
   706
        
tor@1
   707
        return null;
tor@1
   708
    }
tor@1
   709
tor@1
   710
    /** Attempts to sanitize the input buffer */
tor@364
   711
    public static enum Sanitize {
tor@1
   712
        /** Only parse the current file accurately, don't try heuristics */
tor@1
   713
        NEVER, 
tor@1
   714
        /** Perform no sanitization */
tor@1
   715
        NONE, 
tor@364
   716
        /** Try to remove the trailing . or :: at the caret line */
tor@364
   717
        EDITED_DOT, 
tor@1
   718
        /** Try to remove the trailing . or :: at the error position, or the prior
tor@1
   719
         * line, or the caret line */
tor@1
   720
        ERROR_DOT, 
tor@797
   721
        /** Try to remove the initial "if" or "unless" on the block
tor@797
   722
         * in case it's not terminated
tor@797
   723
         */
tor@797
   724
        BLOCK_START,
tor@1
   725
        /** Try to cut out the error line */
tor@1
   726
        ERROR_LINE, 
tor@1
   727
        /** Try to cut out the current edited line, if known */
tor@364
   728
        EDITED_LINE,
tor@364
   729
        /** Attempt to add an "end" to the end of the buffer to make it compile */
tor@364
   730
        MISSING_END,
tor@364
   731
    }
tor@364
   732
tor@364
   733
    /** Parsing context */
tor@364
   734
    public static class Context {
mkrauskopf@3030
   735
        private final Snapshot snapshot;
mkrauskopf@3030
   736
        private final SourceModificationEvent event;
tor@364
   737
        private int errorOffset;
tor@364
   738
        private String source;
tor@364
   739
        private String sanitizedSource;
tor@364
   740
        private OffsetRange sanitizedRange = OffsetRange.NONE;
tor@581
   741
        private String sanitizedContents;
tor@364
   742
        private int caretOffset;
tor@364
   743
        private Sanitize sanitized = Sanitize.NONE;
mkrauskopf@3030
   744
        private ParseErrorHandler errorHandler;
tor@364
   745
        
mkrauskopf@3030
   746
        public Context(Snapshot snapshot, SourceModificationEvent event) {
mkrauskopf@3030
   747
            this.snapshot = snapshot;
mkrauskopf@3030
   748
            this.event = event;
mkrauskopf@3030
   749
            this.source = asString(snapshot.getText());
mkrauskopf@3030
   750
            this.caretOffset = GsfUtilities.getLastKnownCaretOffset(snapshot, event);
tor@364
   751
        }
tor@364
   752
        
tor@364
   753
        @Override
tor@364
   754
        public String toString() {
mkrauskopf@3030
   755
            return "RubyParser.Context(" + snapshot.getSource().getFileObject() + ")"; // NOI18N
tor@364
   756
        }
tor@364
   757
        
tor@364
   758
        public OffsetRange getSanitizedRange() {
tor@364
   759
            return sanitizedRange;
tor@364
   760
        }
tor@364
   761
tor@364
   762
        public Sanitize getSanitized() {
tor@364
   763
            return sanitized;
tor@364
   764
        }
tor@364
   765
        
tor@364
   766
        public String getSanitizedSource() {
tor@364
   767
            return sanitizedSource;
tor@364
   768
        }
tor@364
   769
        
tor@364
   770
        public int getErrorOffset() {
tor@364
   771
            return errorOffset;
tor@364
   772
        }
tor@1
   773
    }
mkrauskopf@3030
   774
mkrauskopf@3030
   775
    private static interface ParseErrorHandler {
mkrauskopf@3030
   776
        void error(Error error);
mkrauskopf@3030
   777
    }
mkrauskopf@3030
   778
emononen@3932
   779
    public static class RubyError implements Error.Badging {
mkrauskopf@3030
   780
        
tor@1291
   781
        private final String displayName;
tor@1291
   782
        private final ID id;
tor@1291
   783
        private final FileObject file;
tor@1291
   784
        private final int startPosition;
tor@1291
   785
        private final int endPosition;
tor@1291
   786
        private final Severity severity;
tor@1291
   787
        private final Object[] parameters;
tor@1291
   788
tor@1291
   789
        public RubyError(String displayName, ID id, FileObject file, int startPosition, int endPosition, Severity severity, Object[] parameters) {
tor@1291
   790
            this.displayName = displayName;
tor@1291
   791
            this.id = id;
tor@1291
   792
            this.file = file;
tor@1291
   793
            this.startPosition = startPosition;
tor@1291
   794
            this.endPosition = endPosition;
tor@1291
   795
            this.severity = severity;
tor@1291
   796
            this.parameters = parameters;
tor@1291
   797
        }
tor@1291
   798
emononen@3932
   799
        @Override
tor@1291
   800
        public String getDisplayName() {
tor@1291
   801
            return displayName;
tor@1291
   802
        }
tor@1291
   803
emononen@3932
   804
        @Override
tor@1291
   805
        public int getStartPosition() {
tor@1291
   806
            return startPosition;
tor@1291
   807
        }
tor@1291
   808
emononen@3932
   809
        @Override
tor@1291
   810
        public int getEndPosition() {
tor@1291
   811
            return endPosition;
tor@1291
   812
        }
tor@1291
   813
emononen@3932
   814
        @Override
tor@1291
   815
        public FileObject getFile() {
tor@1291
   816
            return file;
tor@1291
   817
        }
tor@1291
   818
emononen@3932
   819
        @Override
tor@1291
   820
        public String getKey() {
tor@1291
   821
            return id != null ? id.name() : "";
tor@1291
   822
        }
tor@1291
   823
        
tor@1291
   824
        public ID getId() {
tor@1291
   825
            return id;
tor@1291
   826
        }
tor@1291
   827
emononen@3932
   828
        @Override
tor@1291
   829
        public Object[] getParameters() {
tor@1291
   830
            return parameters;
tor@1291
   831
        }
tor@1291
   832
emononen@3932
   833
        @Override
tor@1291
   834
        public Severity getSeverity() {
tor@1291
   835
            return severity;
tor@1291
   836
        }
tor@1291
   837
tor@1291
   838
        @Override
tor@1291
   839
        public String toString() {
tor@1291
   840
            return "RubyError:" + displayName;
tor@1291
   841
        }
tor@1291
   842
emononen@3932
   843
        @Override
tor@1291
   844
        public String getDescription() {
tor@1291
   845
            return null;
tor@1291
   846
        }
mfukala@3312
   847
emononen@3932
   848
        @Override
mfukala@3312
   849
        public boolean isLineError() {
mfukala@3312
   850
            return true;
mfukala@3312
   851
        }
emononen@3932
   852
emononen@3932
   853
        @Override
emononen@3932
   854
        public boolean showExplorerBadge() {
emononen@4081
   855
            // don't show explored badges for rhtml files,
emononen@4081
   856
            // see #183453
emononen@4081
   857
            return !RubyUtils.isRhtmlFile(file);
emononen@3932
   858
        }
tor@1291
   859
    }    
tor@1
   860
}