diff --git a/.gitignore b/.gitignore index f73759d797..e4699ebfab 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,7 @@ buildNumber.properties .flattened-pom.xml ### VisualStudioCode ### +.vscode .vscode/* !.vscode/settings.json !.vscode/tasks.json diff --git a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java index 5412879513..71f30e491b 100644 --- a/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java +++ b/org.lflang.diagram/src/org/lflang/diagram/synthesis/util/ReactorIcons.java @@ -24,31 +24,27 @@ ***************/ package org.lflang.diagram.synthesis.util; -import com.google.inject.Inject; -import de.cau.cs.kieler.klighd.krendering.KContainerRendering; -import de.cau.cs.kieler.klighd.krendering.KGridPlacementData; -import de.cau.cs.kieler.klighd.krendering.KRectangle; -import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; -import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; -import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; -import java.io.File; -import java.io.IOException; import java.io.InputStream; -import java.lang.ref.SoftReference; -import java.net.URL; import java.util.HashMap; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.emf.common.util.URI; -import org.eclipse.emf.ecore.EObject; + import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; -import org.eclipse.xtext.xbase.lib.Exceptions; import org.eclipse.xtext.xbase.lib.Extension; -import org.eclipse.xtext.xbase.lib.StringExtensions; import org.lflang.ASTUtils; +import org.lflang.AttributeUtils; import org.lflang.diagram.synthesis.AbstractSynthesisExtensions; import org.lflang.lf.ReactorDecl; +import org.lflang.util.FileUtil; + +import com.google.inject.Inject; + +import de.cau.cs.kieler.klighd.krendering.Colors; +import de.cau.cs.kieler.klighd.krendering.KContainerRendering; +import de.cau.cs.kieler.klighd.krendering.KGridPlacementData; +import de.cau.cs.kieler.klighd.krendering.KRectangle; +import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared; +import de.cau.cs.kieler.klighd.krendering.extensions.KContainerRenderingExtensions; +import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions; /** * Utility class to handle icons for reactors in Lingua Franca diagrams. @@ -63,127 +59,91 @@ public class ReactorIcons extends AbstractSynthesisExtensions { private static final ImageLoader LOADER = new ImageLoader(); - // memory-sensitive cache - private static final HashMap> CACHE = new HashMap<>(); + // Image cache during synthesis + private final HashMap cache = new HashMap<>(); + // Error message + private String error = null; public void handleIcon(KContainerRendering rendering, ReactorDecl reactor, boolean collapsed) { if (!collapsed) { return; } - URL iconLocation = locateIcon(reactor); - if (iconLocation != null) { - ImageData data = loadImage(iconLocation); - if (data != null) { - KRectangle figure = _kContainerRenderingExtensions.addRectangle(rendering); - _kRenderingExtensions.setInvisible(figure, true); - KGridPlacementData figurePlacement = _kRenderingExtensions.setGridPlacementData(figure, data.width, data.height); - _kRenderingExtensions.to( - _kRenderingExtensions.from( - figurePlacement, - _kRenderingExtensions.LEFT, 3, 0, - _kRenderingExtensions.TOP, 0, 0), - _kRenderingExtensions.RIGHT, 3, 0, - _kRenderingExtensions.BOTTOM, 3, 0); - - KRectangle icon = _kContainerRenderingExtensions.addRectangle(figure); - _kRenderingExtensions.setInvisible(icon, true); - _kContainerRenderingExtensions.addImage(icon, data); - _kRenderingExtensions.setPointPlacementData(icon, - _kRenderingExtensions.createKPosition( - _kRenderingExtensions.LEFT, 0, 0.5f, - _kRenderingExtensions.TOP, 0, 0.5f), - _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, - 0, data.width, data.height); - } + + // Reset error + error = null; + + // Get annotation + String iconPath = AttributeUtils.findAttributeByName(reactor, "icon"); + if (iconPath == null) { // Fallback to old syntax (in comment) + iconPath = ASTUtils.findAnnotationInComments(reactor, "@icon"); } - } - - private URL locateIcon(EObject eobj) { - URL location = null; - String iconPath = ASTUtils.findAnnotationInComments(eobj, "@icon"); - if (!StringExtensions.isNullOrEmpty(iconPath)) { - // Check if path is URL - try { - return new URL(iconPath); - } catch (Exception e) { - // nothing - } - // Check if path exists as is - File path = new File(iconPath); - if (path.exists()) { - try { - return path.toURI().toURL(); - } catch (Exception e) { - // nothing - } - } - // Check if path is relative to LF file - URI eURI = eobj.eResource() != null ? eobj.eResource().getURI() : null; - if (eURI != null) { - java.net.URI sourceURI = null; - try { - if (eURI.isFile()) { - sourceURI = new java.net.URI(eURI.toString()); - sourceURI = new java.net.URI(sourceURI.getScheme(), null, - sourceURI.getPath().substring(0, sourceURI.getPath().lastIndexOf("/")), null); - } else if (eURI.isPlatformResource()) { - IResource iFile = ResourcesPlugin.getWorkspace().getRoot().findMember(eURI.toPlatformString(true)); - sourceURI = iFile != null ? iFile.getRawLocation().toFile().getParentFile().toURI() : null; - } else if (eURI.isPlatformPlugin()) { - // TODO support loading from plugin bundles? - } - } catch (Exception e) { - // nothing - } - if (sourceURI != null) { - try { - location = sourceURI.resolve(path.toString()).toURL(); - } catch (Exception e) { - // nothing - } + if (iconPath != null && !iconPath.isEmpty()) { + var iconLocation = FileUtil.locateFile(iconPath, reactor.eResource()); + if (iconLocation == null) { + error = "Cannot find given icon file."; + } else { + ImageData data = loadImage(iconLocation); + if (data != null) { + KRectangle figure = _kContainerRenderingExtensions.addRectangle(rendering); + _kRenderingExtensions.setInvisible(figure, true); + KGridPlacementData figurePlacement = _kRenderingExtensions.setGridPlacementData(figure, data.width, data.height); + _kRenderingExtensions.to( + _kRenderingExtensions.from( + figurePlacement, + _kRenderingExtensions.LEFT, 3, 0, + _kRenderingExtensions.TOP, 0, 0), + _kRenderingExtensions.RIGHT, 3, 0, + _kRenderingExtensions.BOTTOM, 3, 0); + KRectangle icon = _kContainerRenderingExtensions.addRectangle(figure); + _kRenderingExtensions.setInvisible(icon, true); + _kContainerRenderingExtensions.addImage(icon, data); + _kRenderingExtensions.setPointPlacementData(icon, + _kRenderingExtensions.createKPosition( + _kRenderingExtensions.LEFT, 0, 0.5f, + _kRenderingExtensions.TOP, 0, 0.5f), + _kRenderingExtensions.H_CENTRAL, _kRenderingExtensions.V_CENTRAL, 0, + 0, data.width, data.height); + } + if (error != null) { + var errorText = _kContainerRenderingExtensions.addText(rendering, "Icon not found!\n"+error); + _kRenderingExtensions.setForeground(errorText, Colors.RED); + _kRenderingExtensions.setFontBold(errorText, true); + _kRenderingExtensions.setSurroundingSpaceGrid(errorText, 8, 0); } } - // TODO more variants based on package and library system in LF } - return location; } - private ImageData loadImage(final URL url) { + private ImageData loadImage(final java.net.URI uri) { try { - synchronized (CACHE) { - if (CACHE.containsKey(url)) { - ImageData img = CACHE.get(url).get(); - if (img != null) { - return img; - } else { - CACHE.remove(url); - } - } + if (cache.containsKey(uri)) { + return cache.get(uri); } synchronized (LOADER) { InputStream inStream = null; try { - inStream = url.openStream(); - // TODO check for memory leak !!! + inStream = uri.toURL().openStream(); ImageData[] data = LOADER.load(inStream); if (data != null && data.length > 0) { ImageData img = data[0]; - synchronized (CACHE) { - CACHE.put(url, new SoftReference(img)); - } + cache.put(uri, img); return img; + } else { + error = "Could not load icon image."; + return null; } - return null; } finally { if (inStream != null) { inStream.close(); } } } - } catch (IOException ex) { - throw Exceptions.sneakyThrow(ex); + } catch (Exception ex) { + ex.printStackTrace(); + error = "Could not load icon image."; + return null; } } diff --git a/org.lflang/src/lib/ts/package.json b/org.lflang/src/lib/ts/package.json index f8ea477625..75897da3e7 100644 --- a/org.lflang/src/lib/ts/package.json +++ b/org.lflang/src/lib/ts/package.json @@ -2,20 +2,15 @@ "name": "LinguaFrancaDefault", "version": "0.0.1", "description": "A default Lingua Franca project for the TypeScript target", + "type": "commonjs", "repository": { "type": "git", "url": "https://github.com/icyphy/lingua-franca" }, "license": "BSD-2-Clause", "dependencies": { - "command-line-args": "^5.1.1", - "command-line-usage": "^6.1.0", - "microtime": "^3.0.0", - "ulog": "^2.0.0-beta.7", - "google-protobuf": "^3.7.4", - "uuid": "^8.3.2" - }, - "devDependencies": { + "reactor-ts": "file:./reactor-ts", + "@types/reactor-ts": "file:./reactor-ts", "@babel/cli": "^7.8.4", "@babel/core": "^7.8.7", "@babel/node": "^7.8.7", @@ -25,18 +20,22 @@ "@babel/plugin-transform-modules-commonjs": "^7.8.3", "@babel/preset-env": "^7.8.7", "@babel/preset-typescript": "^7.8.3", + "command-line-usage": "^6.1.0", + "command-line-args": "^5.1.1", + "rimraf": "^3.0.2" + }, + "devDependencies": { "@types/google-protobuf": "^3.7.4", "@types/node": "^13.9.2", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.8.1", + "@typescript-eslint/eslint-plugin": "5.33.0", "@typescript-eslint/parser": "^5.8.1", "eslint": "^8.5.0", - "rimraf": "^3.0.2", - "typescript": "^3.8.3", + "typescript": "~4.8.2", "ts-protoc-gen": "^0.12.0" }, "scripts": { "check-types": "tsc", - "build": "rimraf dist && babel src --out-dir dist --extensions .ts,.js" + "build": "npx rimraf dist && npx babel src --out-dir dist --extensions .ts,.js" } } diff --git a/org.lflang/src/lib/ts/reactor-ts b/org.lflang/src/lib/ts/reactor-ts index 05f87e5542..d8356fb0d7 160000 --- a/org.lflang/src/lib/ts/reactor-ts +++ b/org.lflang/src/lib/ts/reactor-ts @@ -1 +1 @@ -Subproject commit 05f87e55422607882a9b0b15f86c8e21c1cfa36d +Subproject commit d8356fb0d78b1f546837bda6611cc19ec8dfca12 diff --git a/org.lflang/src/lib/ts/tsconfig.json b/org.lflang/src/lib/ts/tsconfig.json index af0b62567e..104b89a4c2 100644 --- a/org.lflang/src/lib/ts/tsconfig.json +++ b/org.lflang/src/lib/ts/tsconfig.json @@ -3,6 +3,7 @@ "allowJs": true, "noEmit": true, "target": "esnext", + "types": ["node", "reactor-ts", "ulog", "microtime", "command-line-args", "command-line-usage"], "esModuleInterop": true, "isolatedModules": true, "lib": ["esnext", "dom"], @@ -12,8 +13,9 @@ "strictBindCallApply": true, "strictNullChecks": true, "strictFunctionTypes": true, + "typeRoots": ["./node_modules/@types/", "./node_modules/reactor-ts/src/core/@types/"] }, "include": [ - "src/**/*", + "src/**/*" ] } diff --git a/org.lflang/src/org/lflang/AttributeUtils.java b/org.lflang/src/org/lflang/AttributeUtils.java index 79d2baf80b..fcafc3cdab 100644 --- a/org.lflang/src/org/lflang/AttributeUtils.java +++ b/org.lflang/src/org/lflang/AttributeUtils.java @@ -41,8 +41,10 @@ /** * A helper class for processing attributes in the AST. + * * @author{Shaokai Lin } * @author{Clément Fournier, TU Dresden, INSA Rennes} + * @author{Alexander Schulz-Rosengarten } */ public class AttributeUtils { @@ -74,20 +76,30 @@ public static List getAttributes(EObject node) { } /** - * Return the value of the {@code @label} attribute if - * present, otherwise return null. + * Return the value of the attribute with the given name + * if present, otherwise return null. * * @throws IllegalArgumentException If the node cannot have attributes */ - public static String findLabelAttribute(EObject node) { + public static String findAttributeByName(EObject node, String name) { List attrs = getAttributes(node); return attrs.stream() - .filter(it -> it.getAttrName().equals("label")) + .filter(it -> it.getAttrName().equalsIgnoreCase(name)) // case-insensitive search (more user-friendly) .map(it -> it.getAttrParms().get(0).getValue().getStr()) .findFirst() .orElse(null); } + /** + * Return the value of the {@code @label} attribute if + * present, otherwise return null. + * + * @throws IllegalArgumentException If the node cannot have attributes + */ + public static String findLabelAttribute(EObject node) { + return findAttributeByName(node, "label"); + } + /** * Return true if the specified node is an Input and has an {@code @sparse} * attribute. diff --git a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt index 7549d2c78b..b2687848df 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSFileConfig.kt @@ -61,19 +61,19 @@ class TSFileConfig( /** * Path to TypeScript core source code. */ - fun tsCoreGenPath(): Path = tsSrcGenPath().resolve("core") + fun reactorTsPath(): Path = srcGenPath.resolve("reactor-ts") /** * Path to the generated docker file */ fun tsDockerFilePath(tsFileName: String): Path { - return srcGenPath.resolve(tsFileName + ".Dockerfile") + return srcGenPath.resolve("$tsFileName.Dockerfile") } - + /** * Path to the generated docker compose file */ fun tsDockerComposeFilePath(): Path { return srcGenPath.resolve("docker-compose.yml") } -} \ No newline at end of file +} diff --git a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt index 9cb49f4f7b..58eb3cef79 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSGenerator.kt @@ -82,7 +82,7 @@ class TSGenerator( ) : GeneratorBase(tsFileConfig, errorReporter) { companion object { - /** Path to the Cpp lib directory (relative to class path) */ + /** Path to the TS lib directory (relative to class path) */ const val LIB_PATH = "/lib/ts" /** @@ -91,14 +91,7 @@ class TSGenerator( */ val CONFIG_FILES = arrayOf("package.json", "tsconfig.json", "babel.config.js", ".eslintrc.json") - /** - * Files to be copied from the reactor-ts submodule into the generated - * source directory. - */ - val RUNTIME_FILES = arrayOf("action.ts", "bank.ts", "cli.ts", "command-line-args.d.ts", - "command-line-usage.d.ts", "component.ts", "event.ts", "federation.ts", "internal.ts", - "reaction.ts", "reactor.ts", "microtime.d.ts", "multiport.ts", "nanotimer.d.ts", "port.ts", - "state.ts", "strings.ts", "time.ts", "trigger.ts", "types.ts", "ulog.d.ts", "util.ts") + val RT_CONFIG_FILES = arrayOf("package.json", "package-lock.json", "tsconfig.json", ".babelrc") private val VG = ExpressionGenerator(::timeInTargetLanguage) { param -> "this.${param.name}.get()" } @@ -155,6 +148,7 @@ class TSGenerator( clean(context) copyRuntime() + collectDependencies(resource, context, tsFileConfig.reactorTsPath(), true) copyConfigFiles() val codeMaps = HashMap() @@ -172,7 +166,7 @@ class TSGenerator( context.reportProgress( "Code generation complete. Collecting dependencies...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - if (shouldCollectDependencies(context)) collectDependencies(resource, context) + if (shouldCollectDependencies(context)) collectDependencies(resource, context, tsFileConfig.srcGenPkgPath, false) if (errorsOccurred()) { context.unsuccessfulFinish(); return; @@ -213,10 +207,15 @@ class TSGenerator( * Copy the TypeScript runtime so that it is accessible to the generated code. */ private fun copyRuntime() { - for (runtimeFile in RUNTIME_FILES) { + FileUtil.copyDirectoryFromClassPath( + "$LIB_PATH/reactor-ts/src/core", + tsFileConfig.reactorTsPath().resolve("src").resolve("core"), + true + ) + for (configFile in RT_CONFIG_FILES) { FileUtil.copyFileFromClassPath( - "$LIB_PATH/reactor-ts/src/core/$runtimeFile", - tsFileConfig.tsCoreGenPath().resolve(runtimeFile) + "$LIB_PATH/reactor-ts/$configFile", + tsFileConfig.reactorTsPath().resolve(configFile) ) } } @@ -356,17 +355,14 @@ class TSGenerator( * @param resource The Lingua Franca source file at * which to report any errors * @param context The context of this build. + * @param path The directory for which to get dependencies. + * @param production Whether to get production dependencies only. */ - private fun collectDependencies(resource: Resource, context: LFGeneratorContext) { + private fun collectDependencies(resource: Resource, context: LFGeneratorContext, path: Path, production: Boolean) { Files.createDirectories(fileConfig.srcGenPkgPath) // may throw - val pnpmInstall = commandFactory.createCommand( - "pnpm", - listOf("install"), - fileConfig.srcGenPkgPath, - false // only produce a warning if command is not found - ) + val pnpmInstall = commandFactory.createCommand("pnpm", if (production) listOf("install", "--prod") else listOf("install"), path, false) // Attempt to use pnpm, but fall back on npm if it is not available. if (pnpmInstall != null) { @@ -381,7 +377,7 @@ class TSGenerator( errorReporter.reportWarning( "Falling back on npm. To prevent an accumulation of replicated dependencies, " + "it is highly recommended to install pnpm globally (npm install -g pnpm).") - val npmInstall = commandFactory.createCommand("npm", listOf("install"), fileConfig.srcGenPkgPath) + val npmInstall = commandFactory.createCommand("npm", if (production) listOf("install", "--production") else listOf("install"), path) if (npmInstall == null) { errorReporter.reportError(NO_NPM_MESSAGE) diff --git a/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt b/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt index 2855be06ad..7fa9c26e22 100644 --- a/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt +++ b/org.lflang/src/org/lflang/generator/ts/TSImportPreambleGenerator.kt @@ -51,18 +51,18 @@ class TSImportPreambleGenerator( const val DEFAULT_IMPORTS = """ |import commandLineArgs from 'command-line-args' |import commandLineUsage from 'command-line-usage' - |import {Parameter as __Parameter, Timer as __Timer, Reactor as __Reactor, App as __App} from './core/reactor' - |import {Action as __Action, Startup as __Startup, FederatePortAction as __FederatePortAction} from './core/action' - |import {Bank as __Bank} from './core/bank' - |import {FederatedApp as __FederatedApp} from './core/federation' - |import {InPort as __InPort, OutPort as __OutPort, Port as __Port} from './core/port' - |import {InMultiPort as __InMultiPort, OutMultiPort as __OutMultiPort} from './core/multiport' - |import {Reaction as __Reaction} from './core/reaction' - |import {State as __State} from './core/state' - |import {TimeUnit, TimeValue, Tag as __Tag, Origin as __Origin} from './core/time' - |import {Args as __Args, Variable as __Variable, Triggers as __Triggers, Present, Read, Write, ReadWrite, MultiReadWrite, Sched} from './core/types' - |import {Log} from './core/util' - |import {ProcessedCommandLineArgs as __ProcessedCommandLineArgs, CommandLineOptionDefs as __CommandLineOptionDefs, CommandLineUsageDefs as __CommandLineUsageDefs, CommandLineOptionSpec as __CommandLineOptionSpec, unitBasedTimeValueCLAType as __unitBasedTimeValueCLAType, booleanCLAType as __booleanCLAType} from './core/cli' + |import {Parameter as __Parameter, Timer as __Timer, Reactor as __Reactor, App as __App} from 'reactor-ts' + |import {Action as __Action, Startup as __Startup, FederatePortAction as __FederatePortAction} from 'reactor-ts' + |import {Bank as __Bank} from 'reactor-ts' + |import {FederatedApp as __FederatedApp} from 'reactor-ts' + |import {InPort as __InPort, OutPort as __OutPort, Port as __Port} from 'reactor-ts' + |import {InMultiPort as __InMultiPort, OutMultiPort as __OutMultiPort} from 'reactor-ts' + |import {Reaction as __Reaction} from 'reactor-ts' + |import {State as __State} from 'reactor-ts' + |import {TimeUnit, TimeValue, Tag as __Tag, Origin as __Origin} from 'reactor-ts' + |import {Args as __Args, Variable as __Variable, Triggers as __Triggers, Present, Read, Write, ReadWrite, MultiReadWrite, Sched} from 'reactor-ts' + |import {Log} from 'reactor-ts' + |import {ProcessedCommandLineArgs as __ProcessedCommandLineArgs, CommandLineOptionDefs as __CommandLineOptionDefs, CommandLineUsageDefs as __CommandLineUsageDefs, CommandLineOptionSpec as __CommandLineOptionSpec, unitBasedTimeValueCLAType as __unitBasedTimeValueCLAType, booleanCLAType as __booleanCLAType} from 'reactor-ts' |""" } diff --git a/org.lflang/src/org/lflang/util/FileUtil.java b/org.lflang/src/org/lflang/util/FileUtil.java index 9f03dff65c..e82249bd8f 100644 --- a/org.lflang/src/org/lflang/util/FileUtil.java +++ b/org.lflang/src/org/lflang/util/FileUtil.java @@ -104,6 +104,59 @@ public static IPath toIPath(URI uri) throws IOException { public static String toUnixString(Path path) { return path.toString().replace('\\', '/'); } + + /** + * Parse the string as file location and return it as URI. + * Supports URIs, plain file paths, and paths relative to a model. + * + * @param path the file location as string. + * @param resource the model resource this file should be resolved relatively. May be null. + * @return the (Java) URI or null if no file can be located. + */ + public static java.net.URI locateFile(String path, Resource resource) { + // Check if path is URL + try { + var uri = new java.net.URI(path); + if(uri.getScheme() != null) { // check if path was meant to be a URI + return uri; + } + } catch (Exception e) { + // nothing + } + // Check if path exists as it is + File file = new File(path); + if (file.exists()) { + try { + return file.toURI(); + } catch (Exception e) { + // nothing + } + } + // Check if path is relative to LF file + if (resource != null) { + URI eURI = resource.getURI(); + if (eURI != null) { + java.net.URI sourceURI = null; + try { + if (eURI.isFile()) { + sourceURI = new java.net.URI(eURI.toString()); + sourceURI = new java.net.URI(sourceURI.getScheme(), null, + sourceURI.getPath().substring(0, sourceURI.getPath().lastIndexOf("/")), null); + } else if (eURI.isPlatformResource()) { + IResource iFile = ResourcesPlugin.getWorkspace().getRoot().findMember(eURI.toPlatformString(true)); + sourceURI = iFile != null ? iFile.getRawLocation().toFile().getParentFile().toURI() : null; + } + if (sourceURI != null) { + return sourceURI.resolve(path.toString()); + } + } catch (Exception e) { + // nothing + } + } + } + // fail + return null; + } /** * Recursively copies the contents of the given 'src' diff --git a/org.lflang/src/org/lflang/validation/AttributeSpec.java b/org.lflang/src/org/lflang/validation/AttributeSpec.java index 35d29da6f6..75fecc48d3 100644 --- a/org.lflang/src/org/lflang/validation/AttributeSpec.java +++ b/org.lflang/src/org/lflang/validation/AttributeSpec.java @@ -204,5 +204,9 @@ enum AttrParamType { )); // @sparse ATTRIBUTE_SPECS_BY_NAME.put("sparse", new AttributeSpec(null)); + // @icon("value") + ATTRIBUTE_SPECS_BY_NAME.put("icon", new AttributeSpec( + List.of(new AttrParamSpec(AttributeSpec.VALUE_ATTR, AttrParamType.STRING, null)) + )); } } diff --git a/org.lflang/src/org/lflang/validation/LFValidator.java b/org.lflang/src/org/lflang/validation/LFValidator.java index d14b792ce1..8e13948bc8 100644 --- a/org.lflang/src/org/lflang/validation/LFValidator.java +++ b/org.lflang/src/org/lflang/validation/LFValidator.java @@ -34,6 +34,7 @@ import static org.lflang.ASTUtils.toDefinition; import static org.lflang.ASTUtils.toOriginalText; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -57,6 +58,7 @@ import org.eclipse.xtext.validation.CheckType; import org.eclipse.xtext.validation.ValidationMessageAcceptor; import org.lflang.ASTUtils; +import org.lflang.AttributeUtils; import org.lflang.ModelInfo; import org.lflang.Target; import org.lflang.TargetProperty; @@ -1253,6 +1255,37 @@ public void checkWidthSpec(WidthSpec widthSpec) { } } } + + @Check(CheckType.FAST) + public void checkReactorIconAttribute(Reactor reactor) { + var attrs = AttributeUtils.getAttributes(reactor); + var iconAttr = attrs.stream() + .filter(it -> it.getAttrName().equalsIgnoreCase("icon")) + .findFirst() + .orElse(null); + if (iconAttr != null) { + var path = iconAttr.getAttrParms().get(0).getValue().getStr(); + + // Check file extension + var validExtensions = Set.of("bmp", "png", "gif", "ico", "jpeg"); + var extensionStrart = path.lastIndexOf("."); + var extension = extensionStrart != -1 ? path.substring(extensionStrart + 1) : ""; + if (!validExtensions.contains(extension.toLowerCase())) { + warning("File extension '" + extension + "' is not supported. Provide any of: " + String.join(", ", validExtensions), + iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE); + return; + } + + // Check file location + var iconLocation = FileUtil.locateFile(path, reactor.eResource()); + if (iconLocation == null) { + warning("Cannot locate icon file.", iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE); + } + if (("file".equals(iconLocation.getScheme()) || iconLocation.getScheme() == null) && !(new File(iconLocation.getPath()).exists())) { + warning("Icon does not exist.", iconAttr.getAttrParms().get(0), Literals.ATTR_PARM__VALUE); + } + } + } @Check(CheckType.FAST) public void checkInitialMode(Reactor reactor) { diff --git a/test/README.md b/test/README.md index 6198dc3ebe..284706e6b6 100644 --- a/test/README.md +++ b/test/README.md @@ -9,7 +9,7 @@ The simplest way to run the regression tests is to use a Bash script called `run run-lf-tests C run-lf-tests Cpp run-lf-tests Python -run-lf-tests TS +run-lf-tests TypeScript ``` You can also selectively run just some of the tests. For example, to run the system tests for an individual target language, do this: