com.palantir.typescript.TypeScriptBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.palantir.typescript.TypeScriptBuilder.java

Source

/*
 * Copyright 2013 Palantir Technologies, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.palantir.typescript;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.ICommand;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;

import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.palantir.typescript.TypeScriptProjects.Folders;
import com.palantir.typescript.preferences.ProjectPreferenceStore;
import com.palantir.typescript.services.language.DiagnosticEx;
import com.palantir.typescript.services.language.FileDelta;
import com.palantir.typescript.services.language.FileDelta.Delta;
import com.palantir.typescript.services.language.LanguageEndpoint;
import com.palantir.typescript.services.language.OutputFile;
import com.palantir.typescript.services.language.TodoCommentEx;

/**
 * The TypeScript builder transpiles TypeScript files into JavaScript.
 *
 * @author dcicerone
 */
public final class TypeScriptBuilder extends IncrementalProjectBuilder {

    public static final String ID = "com.palantir.typescript.typeScriptBuilder";

    private static final String PROBLEM_MARKER_TYPE = "com.palantir.typescript.typeScriptProblem";
    private static final String TASK_MARKER_TYPE = "com.palantir.typescript.typeScriptTask";

    private final LanguageEndpoint languageEndpoint;

    public TypeScriptBuilder() {
        this.languageEndpoint = TypeScriptPlugin.getDefault().getBuilderLanguageEndpoint();
    }

    @Override
    protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException {
        checkNotNull(monitor);

        // incremental or full build
        switch (kind) {
        case IncrementalProjectBuilder.AUTO_BUILD:
        case IncrementalProjectBuilder.INCREMENTAL_BUILD:
            this.incrementalBuild(monitor);
            break;
        case IncrementalProjectBuilder.FULL_BUILD:
            this.fullBuild(monitor);
            break;
        }

        return null;
    }

    @Override
    protected void clean(IProgressMonitor monitor) throws CoreException {
        checkNotNull(monitor);

        // delete built files if compile-on-save is enabled
        IPreferenceStore projectPreferenceStore = new ProjectPreferenceStore(this.getProject());
        if (projectPreferenceStore.getBoolean(IPreferenceConstants.COMPILER_COMPILE_ON_SAVE)
                && !isOutputFileSpecified()) {
            Set<FileDelta> fileDeltas = getAllSourceFiles(Delta.REMOVED);

            this.clean(fileDeltas, monitor);
        }

        // clean the language service in case it is out-of-sync
        this.languageEndpoint.cleanProject(this.getProject());

        this.deleteAllMarkers();
    }

    @Override
    protected void startupOnInitialize() {
        super.startupOnInitialize();

        this.languageEndpoint.initializeProject(this.getProject());
    }

    private void fullBuild(IProgressMonitor monitor) throws CoreException {
        Set<FileDelta> fileDeltas = this.getAllSourceFiles(Delta.ADDED);

        // initialize the project in the language service to ensure it is up-to-date
        this.languageEndpoint.initializeProject(this.getProject());

        this.build(fileDeltas, monitor);
    }

    private void incrementalBuild(IProgressMonitor monitor) throws CoreException {
        IProject project = this.getProject();
        IResourceDelta delta = this.getDelta(project);

        // update all source and exported files in the language service
        Set<FileDelta> allFileDeltas = TypeScriptProjects.getFileDeltas(project, Folders.SOURCE_AND_EXPORTED,
                delta);
        if (!allFileDeltas.isEmpty()) {
            this.languageEndpoint.updateFiles(allFileDeltas);
        }

        // build the modified source files
        Set<FileDelta> sourceFileDeltas = TypeScriptProjects.getFileDeltas(project, Folders.SOURCE, delta);
        if (!sourceFileDeltas.isEmpty()) {
            // replace the file deltas with all the source files if an output file is specified
            if (this.isOutputFileSpecified()) {
                sourceFileDeltas = this.getAllSourceFiles(Delta.ADDED);
            }

            this.build(sourceFileDeltas, monitor);
        }

        // re-create the markers for projects which reference this one
        for (IProject referencingProject : this.getProject().getReferencingProjects()) {
            if (!this.languageEndpoint.isProjectInitialized(referencingProject)) {
                this.languageEndpoint.initializeProject(referencingProject);
            }

            this.deleteAllMarkers(referencingProject);
            this.createMarkers(referencingProject, monitor);
        }
    }

    private void build(Set<FileDelta> fileDeltas, IProgressMonitor monitor) throws CoreException {
        IPreferenceStore projectPreferenceStore = new ProjectPreferenceStore(this.getProject());

        this.deleteAllMarkers();

        // compile the source files if compile-on-save is enabled
        if (projectPreferenceStore.getBoolean(IPreferenceConstants.COMPILER_COMPILE_ON_SAVE)) {
            this.ensureOutputFolderExists(monitor);

            if (isOutputFileSpecified()) {
                String fileName = null;

                // pick the first non-definition file as the one to "compile" (like a full build)
                for (FileDelta fileDelta : fileDeltas) {
                    String deltaFileName = fileDelta.getFileName();

                    if (!isDefinitionFile(deltaFileName)) {
                        fileName = deltaFileName;
                        break;
                    }
                }

                if (fileName != null) {
                    this.compile(fileName, monitor);
                }
            } else {
                this.clean(fileDeltas, monitor);
                this.compile(fileDeltas, monitor);
            }
        }

        // create the problem markers
        this.createMarkers(this.getProject(), monitor);
    }

    private void deleteAllMarkers(IProject project) throws CoreException {
        project.deleteMarkers(PROBLEM_MARKER_TYPE, true, IResource.DEPTH_INFINITE);
        project.deleteMarkers(TASK_MARKER_TYPE, true, IResource.DEPTH_INFINITE);
    }

    private void deleteAllMarkers() throws CoreException {
        this.deleteAllMarkers(this.getProject());
    }

    private void ensureOutputFolderExists(IProgressMonitor monitor) throws CoreException {
        IPreferenceStore projectPreferenceStore = new ProjectPreferenceStore(this.getProject());

        // ensure the output directory exists if it was specified
        String outputFolderName = projectPreferenceStore.getString(IPreferenceConstants.COMPILER_OUT_DIR);
        if (!Strings.isNullOrEmpty(outputFolderName)) {
            IFolder outputFolder = this.getProject().getFolder(outputFolderName);

            if (!outputFolder.exists()) {
                EclipseResources.createParentDirs(outputFolder, monitor);

                // mark the folder as derived so built resources don't show up in file searches
                outputFolder.setDerived(true, monitor);
            }
        }
    }

    private Set<FileDelta> getAllSourceFiles(Delta withDelta) {
        ImmutableSet.Builder<FileDelta> fileDeltas = ImmutableSet.builder();

        IProject project = this.getProject();
        Set<IFile> files = TypeScriptProjects.getFiles(project, Folders.SOURCE);
        for (IFile file : files) {
            fileDeltas.add(new FileDelta(withDelta, file));
        }

        return fileDeltas.build();
    }

    private void clean(Set<FileDelta> fileDeltas, IProgressMonitor monitor) {
        IPath commonSourcePath = TypeScriptProjects.getCommonSourcePath(this.getProject());
        IContainer outputFolder = TypeScriptProjects.getOutputFolder(this.getProject());

        Set<FileDelta> deletedEmittedOutputToSend = Sets.newHashSet();
        for (FileDelta fileDelta : fileDeltas) {
            Delta delta = fileDelta.getDelta();

            if (delta == Delta.REMOVED) {
                String removedFileName = fileDelta.getFileName();
                IPath removedFilePath = EclipseResources.getFile(removedFileName).getFullPath();

                // skip definition files
                if (isDefinitionFile(removedFileName)) {
                    continue;
                }

                IFile definitionFile = deleteEmittedFile(removedFilePath, "d.ts", commonSourcePath, outputFolder,
                        monitor);
                deleteEmittedFile(removedFilePath, "js", commonSourcePath, outputFolder, monitor);
                deleteEmittedFile(removedFilePath, "js.map", commonSourcePath, outputFolder, monitor);

                if (definitionFile != null) {
                    deletedEmittedOutputToSend.add(new FileDelta(Delta.REMOVED, definitionFile));
                }
            }
        }

        this.languageEndpoint.updateFiles(deletedEmittedOutputToSend);
    }

    private void compile(Set<FileDelta> fileDeltas, IProgressMonitor monitor) throws CoreException {
        for (FileDelta fileDelta : fileDeltas) {
            Delta delta = fileDelta.getDelta();

            if (delta == Delta.ADDED || delta == Delta.CHANGED) {
                String fileName = fileDelta.getFileName();

                // skip definition files
                if (isDefinitionFile(fileName)) {
                    continue;
                }

                // compile the file
                try {
                    this.compile(fileName, monitor);
                } catch (RuntimeException e) {
                    String errorMessage = "Compilation of '" + fileName + "' failed.";
                    Status status = new Status(IStatus.ERROR, TypeScriptPlugin.ID, errorMessage, e);

                    TypeScriptPlugin.getDefault().getLog().log(status);
                }
            }
        }
    }

    private void compile(String fileName, IProgressMonitor monitor) throws CoreException {
        IProject project = this.getProject();
        boolean isProjectReferenced = project.getReferencingProjects().length > 0;

        Set<FileDelta> emittedOutputToSend = Sets.newHashSet();
        for (OutputFile outputFile : this.languageEndpoint.getEmitOutput(project, fileName)) {
            String outputFileName = outputFile.getName();
            IFile eclipseFile = EclipseResources.getFile(outputFileName);
            String filePath = EclipseResources.getFilePath(eclipseFile);
            File file = new File(filePath);

            // write the file
            try {
                Files.createParentDirs(file);
                Files.write(outputFile.getText(), file, Charsets.UTF_8);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            // refresh the file so that eclipse knows about it
            eclipseFile.refreshLocal(IResource.DEPTH_ZERO, monitor);

            // if this output file is going to be referenced by anything, the LanguageEndpoint needs
            // to know about it. we send it back over the bridge because the node side doesn't
            // know the filesystem path of the file and so can't create the FileInfo without this call.
            if (isProjectReferenced
                    && TypeScriptProjects.isContainedInFolders(project, Folders.EXPORTED, eclipseFile)
                    && isDefinitionFile(outputFileName)) {
                emittedOutputToSend.add(new FileDelta(Delta.ADDED, eclipseFile));
            }
        }

        this.languageEndpoint.updateFiles(emittedOutputToSend);
    }

    private boolean isOutputFileSpecified() {
        IPreferenceStore projectPreferenceStore = new ProjectPreferenceStore(this.getProject());

        return !Strings.isNullOrEmpty(projectPreferenceStore.getString(IPreferenceConstants.COMPILER_OUT_FILE));
    }

    private void createMarkers(IProject project, IProgressMonitor monitor) throws CoreException {
        final Map<String, List<DiagnosticEx>> diagnostics = this.languageEndpoint.getAllDiagnostics(project);
        final Map<String, List<TodoCommentEx>> todos = this.languageEndpoint.getAllTodoComments(project);

        // create the markers within a workspace runnable for greater efficiency
        IWorkspaceRunnable runnable = new IWorkspaceRunnable() {
            @Override
            public void run(IProgressMonitor runnableMonitor) throws CoreException {
                createMarkers(diagnostics, todos);
            }
        };
        ResourcesPlugin.getWorkspace().run(runnable, project, IWorkspace.AVOID_UPDATE, monitor);
    }

    public static boolean isConfigured(IProject project) {
        checkNotNull(project);

        IProjectDescription description;
        try {
            description = project.getDescription();
        } catch (CoreException e) {
            throw new RuntimeException(e);
        }

        for (ICommand command : description.getBuildSpec()) {
            if (command.getBuilderName().equals(TypeScriptBuilder.ID)) {
                return true;
            }
        }

        return false;
    }

    private static void createMarkers(Map<String, List<DiagnosticEx>> diagnostics,
            Map<String, List<TodoCommentEx>> todoComments) throws CoreException {

        for (Map.Entry<String, List<DiagnosticEx>> entry : diagnostics.entrySet()) {
            String fileName = entry.getKey();

            // create the problem markers for this file
            IFile file = EclipseResources.getFile(fileName);
            List<DiagnosticEx> fileDiagnostics = entry.getValue();
            for (DiagnosticEx diagnostic : fileDiagnostics) {
                IMarker marker = file.createMarker(PROBLEM_MARKER_TYPE);
                Map<String, Object> attributes = createProblemMarkerAttributes(diagnostic);

                marker.setAttributes(attributes);
            }
        }

        for (Map.Entry<String, List<TodoCommentEx>> entry : todoComments.entrySet()) {
            String fileName = entry.getKey();

            // create the task markers for this file
            IFile file = EclipseResources.getFile(fileName);
            List<TodoCommentEx> fileTodos = entry.getValue();
            for (TodoCommentEx todo : fileTodos) {
                IMarker marker = file.createMarker(TASK_MARKER_TYPE);
                Map<String, Object> attributes = createTaskMarkerAttributes(todo);

                marker.setAttributes(attributes);
            }
        }
    }

    private static Map<String, Object> createProblemMarkerAttributes(DiagnosticEx diagnostic) {
        ImmutableMap.Builder<String, Object> attributes = ImmutableMap.builder();

        attributes.put(IMarker.CHAR_START, diagnostic.getStart());
        attributes.put(IMarker.CHAR_END, diagnostic.getStart() + diagnostic.getLength());
        attributes.put(IMarker.LINE_NUMBER, diagnostic.getLine());
        attributes.put(IMarker.MESSAGE, diagnostic.getText());
        attributes.put(IMarker.PRIORITY, IMarker.PRIORITY_NORMAL);
        attributes.put(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);

        return attributes.build();
    }

    private static Map<String, Object> createTaskMarkerAttributes(TodoCommentEx todo) {
        ImmutableMap.Builder<String, Object> attributes = ImmutableMap.builder();

        attributes.put(IMarker.CHAR_START, todo.getStart());
        attributes.put(IMarker.CHAR_END, todo.getStart() + todo.getText().length());
        attributes.put(IMarker.LINE_NUMBER, todo.getLine());
        attributes.put(IMarker.MESSAGE, todo.getText());
        attributes.put(IMarker.PRIORITY, todo.getPriority());
        attributes.put(IMarker.SEVERITY, IMarker.SEVERITY_INFO);

        return attributes.build();
    }

    private static IFile deleteEmittedFile(IPath sourceFilePath, String extension, IPath commonSourcePath,
            IContainer outputFolder, IProgressMonitor monitor) {

        IPath emittedPath = null;
        if (outputFolder == null || commonSourcePath == null) {
            emittedPath = sourceFilePath.removeFileExtension().addFileExtension(extension);
        } else {
            emittedPath = outputFolder.getFullPath().append(sourceFilePath.makeRelativeTo(commonSourcePath)
                    .removeFileExtension().addFileExtension(extension));
        }

        IFile emittedFile = ResourcesPlugin.getWorkspace().getRoot().getFile(emittedPath);
        try {
            emittedFile.delete(true, monitor);
        } catch (CoreException e) {
            // indicate that nothing was deleted
            return null;
        }
        return emittedFile;
    }

    private static boolean isDefinitionFile(String fileName) {
        return fileName.endsWith(".d.ts");
    }
}