netbeanstypescript.tsconfig.TSConfigParser.java Source code

Java tutorial

Introduction

Here is the source code for netbeanstypescript.tsconfig.TSConfigParser.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2016 Everlaw. All rights reserved.
 *
 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
 * Other names may be trademarks of their respective owners.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */
package netbeanstypescript.tsconfig;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.event.ChangeListener;
import netbeanstypescript.TSService;
import netbeanstypescript.api.lexer.JsTokenId;
import netbeanstypescript.tsconfig.TSConfigCodeCompletion.TSConfigElementHandle;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.Severity;
import org.netbeans.modules.csl.spi.DefaultError;
import org.netbeans.modules.csl.spi.ParserResult;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Task;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.parsing.spi.SourceModificationEvent;
import org.openide.filesystems.FileObject;

/**
    
 * @author jeffrey
 */
public class TSConfigParser extends Parser {

    static class ConfigNode {
        int keyOffset;
        int startOffset;
        int endOffset;
        boolean missing;
        Object value;
        List<ConfigNode> elements;
        HashMap<String, ConfigNode> properties;

        Map<String, TSConfigElementHandle> validMap;
        Object expectedType;
    }

    static class Result extends ParserResult {
        final FileObject fileObj;
        List<Error> errors = new ArrayList<>();
        ConfigNode root;

        Result(Snapshot snapshot) {
            super(snapshot);
            fileObj = snapshot.getSource().getFileObject();
        }

        void addError(String error, ConfigNode node) {
            addError(error, node.startOffset, node.endOffset, Severity.ERROR);
        }

        void addError(String error, int start, int end) {
            addError(error, start, end, Severity.ERROR);
        }

        void addError(String error, int start, int end, Severity sev) {
            errors.add(new DefaultError(null, error, null, fileObj, start, end, false, sev));
        }

        @Override
        public List<? extends Error> getDiagnostics() {
            return errors;
        }

        @Override
        protected void invalidate() {
        }
    }

    Result result;

    @Override
    public void parse(Snapshot snapshot, Task task, SourceModificationEvent sme) throws ParseException {
        final TokenSequence<JsTokenId> ts = snapshot.getTokenHierarchy()
                .tokenSequence(JsTokenId.javascriptLanguage());
        final Result res = new Result(snapshot);

        res.root = (new Object() {
            private JsTokenId advance() {
                while (ts.moveNext()) {
                    JsTokenId id = ts.token().id();
                    String category = id.primaryCategory();
                    if (!("comment".equals(category) || "whitespace".equals(category))) {
                        return id;
                    }
                }
                return null;
            }

            private void error(String msg) {
                res.addError(msg, ts.offset(), ts.offset() + ts.token().length());
            }

            private boolean nextListItem(boolean previous, JsTokenId endToken, String type) {
                if (!previous) {
                    JsTokenId id = advance();
                    if (id == null || id == JsTokenId.BRACKET_RIGHT_BRACKET
                            || id == JsTokenId.BRACKET_RIGHT_CURLY) {
                        if (id != endToken) {
                            if (id != null)
                                ts.movePrevious();
                            error("Unterminated " + type + ".");
                        }
                        return false;
                    }
                    return true;
                } else {
                    JsTokenId id = advance();
                    if (id == JsTokenId.OPERATOR_COMMA) {
                        id = advance();
                        if (id == null) {
                            error("Unterminated " + type + ".");
                            return false;
                        }
                        return true;
                    } else if (id == null || id == JsTokenId.BRACKET_RIGHT_BRACKET
                            || id == JsTokenId.BRACKET_RIGHT_CURLY) {
                        if (id != endToken) {
                            if (id != null)
                                ts.movePrevious();
                            error("Unterminated " + type + ".");
                        }
                        return false;
                    } else {
                        error("Missing comma.");
                        return true;
                    }
                }
            }

            private ConfigNode value() {
                ConfigNode node = new ConfigNode();
                node.startOffset = ts.offset();
                switch (ts.token().id()) {
                case KEYWORD_NULL:
                    break;
                case KEYWORD_TRUE:
                    node.value = Boolean.TRUE;
                    break;
                case KEYWORD_FALSE:
                    node.value = Boolean.FALSE;
                    break;
                case STRING:
                case NUMBER:
                    // TODO: negative numbers
                    try {
                        node.value = JSONValue.parseWithException(ts.token().text().toString());
                    } catch (org.json.simple.parser.ParseException e) {
                        error("Invalid JSON literal: " + e);
                        return null;
                    }
                    break;
                case BRACKET_LEFT_BRACKET:
                    node.elements = new ArrayList<>();
                    for (boolean in = false; (in = nextListItem(in, JsTokenId.BRACKET_RIGHT_BRACKET, "array"));) {
                        ConfigNode element = value();
                        if (element != null) {
                            node.elements.add(element);
                        }
                    }
                    break;
                case BRACKET_LEFT_CURLY:
                    node.properties = new LinkedHashMap<>();
                    for (boolean in = false; (in = nextListItem(in, JsTokenId.BRACKET_RIGHT_CURLY, "object"));) {
                        ConfigNode keyNode = value();
                        if (keyNode == null)
                            continue;
                        String key = null;
                        if (keyNode.value instanceof String) {
                            key = (String) keyNode.value;
                        } else {
                            res.addError("JSON object key must be a string.", keyNode);
                        }
                        int colonOffset = keyNode.endOffset;
                        ConfigNode valueNode;
                        if (advance() == JsTokenId.OPERATOR_COLON) {
                            colonOffset = ts.offset() + 1;
                            advance();
                        } else {
                            res.addError("JSON object must consist of '\"key\": value' pairs.", keyNode);
                        }
                        valueNode = value();
                        if (valueNode == null) {
                            valueNode = new ConfigNode();
                            valueNode.missing = true;
                            valueNode.startOffset = colonOffset;
                            valueNode.endOffset = ts.offset() + ts.token().length();
                        }
                        valueNode.keyOffset = keyNode.startOffset;
                        if (key != null) {
                            ConfigNode oldValue = node.properties.put(key, valueNode);
                            if (oldValue != null) {
                                res.addError("Duplicate key '" + key + "' will be ignored.", oldValue.keyOffset,
                                        oldValue.endOffset, Severity.WARNING);
                            }
                        }
                    }
                    break;
                case OPERATOR_COMMA:
                case BRACKET_RIGHT_BRACKET:
                case BRACKET_RIGHT_CURLY:
                    error("Missing value.");
                    ts.movePrevious();
                    return null;
                default:
                    error("Unexpected token type " + ts.token().id());
                    return null;
                }
                node.endOffset = ts.offset() + ts.token().length();
                return node;
            }

            ConfigNode root() {
                if (advance() == null) {
                    return null;
                }
                ConfigNode root = value();
                if (advance() != null) {
                    error("Extra text at end of JSON.");
                }
                return root;
            }
        }).root();

        ROOT: if (res.root != null) {
            if (res.root.properties == null) {
                res.addError("tsconfig.json value should be an object", res.root);
                break ROOT;
            }

            Map<String, TSConfigElementHandle> rootCompletions = res.root.validMap = new HashMap<>();

            rootCompletions.put("compileOnSave", new TSConfigElementHandle("compileOnSave", "boolean",
                    "If true, any TypeScript files modified during the IDE session are automatically transpiled to JS."));
            ConfigNode compileOnSave = res.root.properties.get("compileOnSave");
            if (compileOnSave != null && !(compileOnSave.value instanceof Boolean)) {
                res.addError("'compileOnSave' value must be a boolean.", compileOnSave);
            }

            ConfigNode files = res.root.properties.get("files");
            rootCompletions.put("files",
                    new TSConfigElementHandle("files", "list", "Array of files to include in the project."));
            if (files != null) {
                if (files.elements == null) {
                    res.addError("'files' value must be an array of strings.", files);
                } else {
                    for (ConfigNode file : files.elements) {
                        if (!(file.value instanceof String)) {
                            res.addError("'files' element should be a string.", file);
                        }
                    }
                }
            }

            ConfigNode include = res.root.properties.get("include");
            rootCompletions.put("include", new TSConfigElementHandle("include", "list",
                    "Array of files and directories to include in the project."));
            if (include != null) {
                if (include.elements == null) {
                    res.addError("'include' value must be an array of strings.", files);
                } else {
                    for (ConfigNode file : include.elements) {
                        if (!(file.value instanceof String)) {
                            res.addError("'include' element should be a string.", file);
                        }
                    }
                }
            }

            ConfigNode exclude = res.root.properties.get("exclude");
            rootCompletions.put("exclude", new TSConfigElementHandle("exclude", "list",
                    "Array of files and directories not to include in the project."));
            if (exclude != null) {
                if (exclude.elements == null) {
                    res.addError("'exclude' value must be an array of strings.", exclude);
                } else {
                    for (ConfigNode file : exclude.elements) {
                        if (!(file.value instanceof String)) {
                            res.addError("'exclude' element should be a string.", file);
                        }
                    }
                }
            }

            rootCompletions.put("compilerOptions", new TSConfigElementHandle("compilerOptions", "object", null));
            ConfigNode compilerOptions = res.root.properties.get("compilerOptions");
            OPTIONS: if (compilerOptions != null) {
                if (compilerOptions.properties == null) {
                    res.addError("'compilerOptions' value should be an object.", compilerOptions);
                    break OPTIONS;
                }
                JSONArray validArray = (JSONArray) TSService.call("getCompilerOptions", res.fileObj);
                if (validArray == null) {
                    res.addError("Error communicating with Node.js process", compilerOptions);
                    break OPTIONS;
                }
                HashMap<String, TSConfigElementHandle> validMap = new HashMap<>();
                for (Object obj : validArray) {
                    TSConfigElementHandle eh = new TSConfigElementHandle((JSONObject) obj);
                    validMap.put(eh.getName(), eh);
                }
                compilerOptions.validMap = validMap;
                for (Map.Entry<String, ConfigNode> entry : compilerOptions.properties.entrySet()) {
                    String key = entry.getKey();
                    ConfigNode value = entry.getValue();
                    TSConfigElementHandle optionInfo = validMap.get(key);
                    if (optionInfo == null) {
                        res.addError("Unknown compiler option '" + key + "'.", value.keyOffset, value.endOffset);
                        continue;
                    }
                    checkType(res, value, optionInfo);
                    if (key.equals("out")) {
                        res.addError("'out' option is deprecated. Use 'outFile' instead.", value.keyOffset,
                                value.endOffset, Severity.WARNING);
                    }
                    if (optionInfo.commandLineOnly) {
                        res.addError("Option '" + key + "' is only meaningful when used from the command line.",
                                value.keyOffset, value.endOffset, Severity.WARNING);
                    }
                }
            }
        }

        this.result = res;
    }

    private void checkType(Result res, ConfigNode value, TSConfigElementHandle optionInfo) {
        String key = optionInfo.name;
        Object type = optionInfo.type;
        value.expectedType = type;
        if (value.missing)
            return;
        if (type instanceof String) {
            boolean valid = false;
            switch ((String) type) {
            case "boolean":
                valid = value.value instanceof Boolean;
                break;
            case "number":
                valid = value.value instanceof Number;
                break;
            case "string":
                valid = value.value instanceof String;
                break;
            case "list":
                if (value.elements != null) {
                    for (ConfigNode element : value.elements) {
                        checkType(res, element, optionInfo.element);
                    }
                    valid = true;
                }
                break;
            case "object":
                valid = value.properties != null;
                break;
            }
            if (!valid) {
                res.addError("Compiler option '" + key + "' requires a value of type " + type + ".", value);
            }
        } else if (type instanceof JSONObject) {
            if (!(value.value instanceof String
                    && ((Map) type).containsKey(((String) value.value).toLowerCase()))) {
                @SuppressWarnings("unchecked")
                List<String> allAllowed = new ArrayList<>(((Map) type).keySet());
                Collections.sort(allAllowed);
                StringBuilder sb = new StringBuilder("Compiler option '").append(key).append("' must be one of: ");
                boolean first = true;
                for (String allowed : allAllowed) {
                    sb.append(first ? "'" : ", '").append(allowed).append('\'');
                    first = false;
                }
                res.addError(sb.append('.').toString(), value);
            }
        }
    }

    @Override
    public Parser.Result getResult(Task task) throws ParseException {
        return result;
    }

    @Override
    public void addChangeListener(ChangeListener cl) {
    }

    @Override
    public void removeChangeListener(ChangeListener cl) {
    }
}