├── js ├── .npmignore ├── index.js ├── eslint.config.js ├── test │ ├── example1.uvl │ ├── fm.test.js │ └── example2.uvl ├── src │ ├── errors │ │ ├── SyntaxGenericError.js │ │ └── ErrorListener.js │ ├── FeatureModel.js │ └── UVLJavaScriptCustomLexer.js ├── package.json └── README.md ├── python ├── __init__.py ├── setup.py ├── main.py ├── uvl │ └── UVLCustomLexer.py └── README.md ├── uvl ├── Python │ ├── UVLPythonParser.g4 │ └── UVLPythonLexer.g4 ├── JavaScript │ ├── UVLJavaScriptParser.g4 │ └── UVLJavaScriptLexer.g4 ├── Java │ ├── UVLJavaParser.g4 │ └── UVLJavaLexer.g4 ├── UVLLexer.g4 └── UVLParser.g4 ├── .gitignore ├── .github ├── workflows │ ├── conventionalpr.yml │ ├── js.yml │ ├── python.yml │ └── java.yml └── settings.xml ├── Makefile ├── README.md ├── LICENSE └── java └── pom.xml /js/.npmignore: -------------------------------------------------------------------------------- 1 | eslint.config.js -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- 1 | from python.main import get_tree 2 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import FeatureModel from './src/FeatureModel.js'; 2 | 3 | export { FeatureModel }; 4 | -------------------------------------------------------------------------------- /uvl/Python/UVLPythonParser.g4: -------------------------------------------------------------------------------- 1 | parser grammar UVLPythonParser; 2 | 3 | options { 4 | tokenVocab = UVLPythonLexer; 5 | } 6 | 7 | import UVLParser; -------------------------------------------------------------------------------- /uvl/JavaScript/UVLJavaScriptParser.g4: -------------------------------------------------------------------------------- 1 | parser grammar UVLJavaScriptParser; 2 | 3 | options { 4 | tokenVocab = UVLJavaScriptLexer; 5 | } 6 | 7 | import UVLParser; -------------------------------------------------------------------------------- /uvl/Java/UVLJavaParser.g4: -------------------------------------------------------------------------------- 1 | parser grammar UVLJavaParser; 2 | 3 | options { 4 | tokenVocab = UVLJavaLexer; 5 | } 6 | 7 | import UVLParser; 8 | 9 | @header { 10 | package uvl; 11 | } -------------------------------------------------------------------------------- /js/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | 5 | export default [ 6 | { 7 | languageOptions: { globals: globals.browser } 8 | }, 9 | pluginJs.configs.recommended, 10 | ]; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode, Eclipse, and IntelliJ IDEA project files 2 | .vscode 3 | .settings 4 | .classpath 5 | .project 6 | .idea 7 | 8 | # Python virtual environments 9 | env 10 | build 11 | python/uvl/__pycache__ 12 | python/uvl/* 13 | !python/uvl/UVLCustomLexer.py 14 | 15 | # Node.js 16 | js/node_modules 17 | js/package-lock.json 18 | 19 | # Java build files 20 | .antlr/ 21 | target/ -------------------------------------------------------------------------------- /.github/workflows/conventionalpr.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commit Parser 2 | on: 3 | pull_request: 4 | branches: [main, master, develop] 5 | types: [opened, reopened, edited, review_requested, synchronize] 6 | 7 | jobs: 8 | conventional_commit: 9 | name: Conventional Commits 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: webiny/action-conventional-commits@v1.0.3 14 | -------------------------------------------------------------------------------- /js/test/example1.uvl: -------------------------------------------------------------------------------- 1 | features 2 | GEMA_SPL 3 | mandatory 4 | GraphicalUserInterface 5 | optional 6 | GUI_Forms 7 | optional 8 | GUI_Lists 9 | optional 10 | GUI_L_Sortable 11 | optional 12 | GUI_L_Filterable 13 | alternative 14 | GUI_L_F_RowFilter 15 | GUI_L_F_BasicSearch 16 | optional 17 | GUI_L_LocateInMap 18 | optional 19 | GUI_L_ViewListAsMap 20 | optional 21 | GUI_L_FormLink -------------------------------------------------------------------------------- /js/src/errors/SyntaxGenericError.js: -------------------------------------------------------------------------------- 1 | export default class SyntaxGenericError extends Error { 2 | constructor(payload) { 3 | super(payload); 4 | 5 | this.code = "E_SYNTAX_GENERIC"; 6 | this.message = "Something went wrong"; 7 | 8 | if (typeof payload !== "undefined") { 9 | this.message = payload.message || this.message; 10 | this.payload = payload; 11 | } 12 | 13 | console.error( 14 | `line ${this.payload.line}, col ${this.payload.column}: ${this.payload.message}`, 15 | ); 16 | Error.captureStackTrace(this, SyntaxGenericError); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /uvl/Python/UVLPythonLexer.g4: -------------------------------------------------------------------------------- 1 | lexer grammar UVLPythonLexer; 2 | import UVLLexer; 3 | 4 | OPEN_PAREN: '(' {self.opened += 1;}; 5 | CLOSE_PAREN: ')' {self.opened -= 1;}; 6 | OPEN_BRACK: '[' {self.opened += 1;}; 7 | CLOSE_BRACK: ']' {self.opened -= 1;}; 8 | OPEN_BRACE: '{' {self.opened += 1;}; 9 | CLOSE_BRACE: '}' {self.opened -= 1;}; 10 | OPEN_COMMENT: '/*' {self.opened += 1;}; 11 | CLOSE_COMMENT: '*/' {self.opened -= 1;}; 12 | 13 | //This is here because the way python manage tabs and new lines 14 | NEWLINE: ( 15 | {self.atStartOfInput()}? SPACES 16 | | ( '\r'? '\n' | '\r') SPACES? 17 | ) {self.handleNewline();}; -------------------------------------------------------------------------------- /.github/workflows/js.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy JS Parser 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 19 18 | registry-url: https://registry.npmjs.org/ 19 | 20 | - name: Install ANTLR4 21 | run: | 22 | make dev 23 | make js_parser 24 | 25 | - name: Generate and Build Node Code 26 | run: cd js && npm install && npm run build-grammar 27 | 28 | - name: Publish npm package 29 | run: cd js && npm publish --access public 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} -------------------------------------------------------------------------------- /js/test/fm.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { FeatureModel } from "./index.js" 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | test('Create a feature model with example1', () => { 7 | const fm = new FeatureModel('test/example1.uvl') 8 | expect(fm.toString().substring(0, 8)).toBe('features') 9 | }); 10 | 11 | test('Create a feature model with example2', () => { 12 | const fm = new FeatureModel('test/example2.uvl') 13 | expect(fm.toString().includes('MapViewer')).toBe(true) 14 | }); 15 | 16 | test('Create a feature model using string', () => { 17 | const filePath = path.resolve('test/example2.uvl'); 18 | const text = fs.readFileSync(filePath, 'utf8'); 19 | const fm = new FeatureModel(text) 20 | expect(fm.toString().includes('MapViewer')).toBe(true) 21 | }); 22 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Python Parser 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Install ANTLR4 and Python dependencies 15 | run: | 16 | # Add installation steps for ANTLR4 here 17 | sudo apt-get update 18 | sudo apt-get install -y python3 python3-pip 19 | make dev 20 | 21 | - name: Generate and Build Python Code 22 | run: make python_parser 23 | 24 | - name: Build and publish python package 25 | env: 26 | TWINE_USERNAME: __token__ 27 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 28 | run: | 29 | make python_prepare_package 30 | cd python && twine upload dist/* 31 | -------------------------------------------------------------------------------- /js/test/example2.uvl: -------------------------------------------------------------------------------- 1 | features 2 | GEMA_SPL 3 | mandatory 4 | MapViewer 5 | mandatory 6 | MV_MapServer 7 | or 8 | MV_MS_GeoServer 9 | MV_MS_GeoJSON 10 | optional 11 | MV_MS_GJ_Cached 12 | optional 13 | MV_MS_GJ_Paginated 14 | mandatory 15 | MV_MapManagement 16 | alternative 17 | MV_MM_UniqueMapViewer 18 | mandatory 19 | MV_MM_UMV_MapCenter 20 | alternative 21 | MV_MM_UMV_MC_BBox 22 | MV_MM_UMV_MC_Coordinates 23 | MV_MM_UMV_MC_UserPosition 24 | MV_MM_MultipleMapViewer 25 | optional 26 | MV_MM_MMV_MapSelectorInMapViewer 27 | optional 28 | MV_MM_MMV_MapSelectorInMenuElement 29 | constraints 30 | MV_MS_GJ_Paginated => !MV_MS_GJ_Cached 31 | MV_MS_GJ_Cached => !MV_MS_GJ_Paginated -------------------------------------------------------------------------------- /js/src/errors/ErrorListener.js: -------------------------------------------------------------------------------- 1 | import antlr4 from "antlr4"; 2 | 3 | import SyntaxGenericError from "./SyntaxGenericError.js"; 4 | 5 | /** 6 | * Custom Error Listener 7 | * 8 | * @returns {object} 9 | */ 10 | class ErrorListener extends antlr4.error.ErrorListener { 11 | /** 12 | * Checks syntax error 13 | * 14 | * @param {object} recognizer The parsing support code essentially. Most of it is error recovery stuff 15 | * @param {object} symbol Offending symbol 16 | * @param {int} line Line of offending symbol 17 | * @param {int} column Position in line of offending symbol 18 | * @param {string} message Error message 19 | * @param {string} payload Stack trace 20 | */ 21 | syntaxError(recognizer, symbol, line, column, message) { 22 | throw new SyntaxGenericError({ line, column, message }); 23 | } 24 | } 25 | 26 | export default ErrorListener; 27 | -------------------------------------------------------------------------------- /.github/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ossrh 8 | ${env.SONATYPE_USERNAME} 9 | ${env.SONATYPE_PASSWORD} 10 | 11 | 12 | 13 | 14 | ossrh 15 | 16 | true 17 | 18 | 19 | gpg 20 | ${env.GPG_PASSPHRASE} 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uvl-parser", 3 | "main": "index.js", 4 | "type": "module", 5 | "version": "0.0.3", 6 | "scripts": { 7 | "build-grammar": "antlr4 -lib ../uvl/ -Dlanguage=JavaScript -o ./src/lib ../uvl/JavaScript/UVLJavaScriptLexer.g4 ../uvl/JavaScript/UVLJavaScriptParser.g4", 8 | "lint": "eslint src --ignore-pattern src/lib/*", 9 | "test": "vitest" 10 | }, 11 | "devDependencies": { 12 | "@eslint/js": "^9.8.0", 13 | "@rollup/plugin-node-resolve": "^15.2.3", 14 | "eslint": "^9.8.0", 15 | "globals": "^15.8.0", 16 | "vitest": "^2.0.4" 17 | }, 18 | "dependencies": { 19 | "antlr4": "4.13.2" 20 | }, 21 | "author": { 22 | "name": "Maria Isabel Limaylla", 23 | "email": "maria.limaylla@udc.es" 24 | }, 25 | "contributors": [ 26 | { 27 | "name": "Victor Lamas", 28 | "email": "victor.lamas@udc.es" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /uvl/JavaScript/UVLJavaScriptLexer.g4: -------------------------------------------------------------------------------- 1 | lexer grammar UVLJavaScriptLexer; 2 | 3 | import UVLLexer; 4 | 5 | @lexer::members { 6 | // Una cola donde se empujan los tokens adicionales (ver la regla del lexer NEWLINE). 7 | let tokens = []; 8 | // La pila que lleva un registro del nivel de indentación. 9 | let indents = []; 10 | // La cantidad de paréntesis, corchetes y llaves abiertos. 11 | let opened = 0; 12 | // El token más recientemente producido. 13 | let lastToken = null; 14 | } 15 | 16 | OPEN_PAREN: '(' {this.opened += 1;}; 17 | CLOSE_PAREN: ')' {this.opened -= 1;}; 18 | OPEN_BRACK: '[' {this.opened += 1;}; 19 | CLOSE_BRACK: ']' {this.opened -= 1;}; 20 | OPEN_BRACE: '{' {this.opened += 1;}; 21 | CLOSE_BRACE: '}' {this.opened -= 1;}; 22 | OPEN_COMMENT: '/*' {this.opened += 1;}; 23 | CLOSE_COMMENT: '*/' {this.opened -= 1;}; 24 | 25 | NEWLINE: ( 26 | {this.atStartOfInput()}? SPACES 27 | | ( '\r'? '\n' | '\r') SPACES? 28 | ) {this.handleNewline();}; -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="uvlparser", 8 | version="2.0.1", 9 | description="This module provides a get_tree function to obtain an ANTLR parse-tree from a UVL-defined feature model", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | url="https://github.com/Universal-Variability-Language/uvl-parser", 13 | author="UVL Team", 14 | author_email="jagalindo@us.es", 15 | # To find more licenses or classifiers go to: https://pypi.org/classifiers/ 16 | license="GNU General Public License v3 (GPLv3)", 17 | packages=['.','uvl'], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 21 | "Operating System :: OS Independent", 22 | ], 23 | zip_safe=True, 24 | python_requires=">=3.0", 25 | install_requires=[ 26 | "antlr4-python3-runtime==4.13.1", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /.github/workflows/java.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Java Parser 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Install ANTLR4 and Maven 15 | run: | 16 | # Add installation steps for ANTLR4 here 17 | sudo apt-get update 18 | sudo apt-get install -y maven 19 | make dev 20 | 21 | - name: Import GPG key 22 | uses: crazy-max/ghaction-import-gpg@v1 23 | env: 24 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 25 | PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 26 | 27 | - name: Generate and Compile Java Code 28 | run: make java_parser 29 | 30 | - name: Cache Maven packages 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.m2 34 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 35 | restore-keys: ${{ runner.os }}-m2 36 | 37 | - name: Deploy to Maven Central 38 | run: cd java && mvn clean deploy --settings ../.github/settings.xml 39 | env: 40 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 41 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 42 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} -------------------------------------------------------------------------------- /js/src/FeatureModel.js: -------------------------------------------------------------------------------- 1 | 2 | import UVLJavaScriptCustomLexer from './UVLJavaScriptCustomLexer.js'; 3 | import UVLJavaScriptParser from './lib/UVLJavaScriptParser.js'; 4 | import ErrorListener from "./errors/ErrorListener.js"; 5 | import antlr4 from 'antlr4'; 6 | import fs from 'fs'; 7 | 8 | export default class FeatureModel { 9 | 10 | constructor(param) { 11 | this.featureModel = ''; 12 | let chars = ''; 13 | if (this.isFile(param)) { 14 | chars = new antlr4.FileStream(param); 15 | } else { 16 | chars = antlr4.CharStreams.fromString(param); 17 | } 18 | this.getTree(chars); 19 | } 20 | 21 | isFile(str) { 22 | try { 23 | return fs.statSync(str); 24 | } catch (e) { 25 | console.error('Error: ' + e); 26 | return false; 27 | } 28 | } 29 | 30 | getTree(chars) { 31 | const lexer = new UVLJavaScriptCustomLexer(chars); 32 | const tokens = new antlr4.CommonTokenStream(lexer); 33 | const errorListener = new ErrorListener(); 34 | let parser = new UVLJavaScriptParser(tokens); 35 | parser.removeErrorListeners(); 36 | parser.addErrorListener(errorListener); 37 | const tree = parser.featureModel(); 38 | this.featureModel = tree; 39 | } 40 | 41 | getFeatureModel() { 42 | return this.featureModel; 43 | } 44 | 45 | toString() { 46 | return this.featureModel.getText(); 47 | } 48 | } 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # UVL Parser Javascript 2 | 3 | This is a parser for the UVL (Unified Variability Language) written in Javascript. The parser is based on the ANTLR4 grammar for UVL. 4 | 5 | index | content 6 | --- | --- 7 | [Installation](#installation) | How to install the parser 8 | [Usage](#usage) | How to use the parser 9 | [Development](#development) | How to develop the parser 10 | [Publishing in npm](#publishing-in-npm) | How to publish the parser in npm 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install uvl-parser 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### ES6 21 | 22 | ```javascript 23 | import { FeatureModel } from 'uvl-parser'; 24 | const featureModel = new FeatureModel('file.uvl'); 25 | const tree = featureModel.getFeatureModel(); 26 | ``` 27 | 28 | ## Development 29 | 30 | ### Install dependencies 31 | 32 | ```bash 33 | npm install 34 | ``` 35 | 36 | ### Build grammar 37 | 38 | ```bash 39 | npm run build-grammar 40 | ``` 41 | 42 | ### Run tests 43 | 44 | ```bash 45 | npm test 46 | ``` 47 | 48 | ## Publishing in npm 49 | 50 | The run build will create the folder with the compiled code. To publish in npm, run the following commands: 51 | 52 | ```bash 53 | npm run build 54 | npm publish --access public 55 | ``` 56 | 57 | ## Authors 58 | 59 | - Victor Lamas: [victor.lamas@udc.es](mailto:victor.lamas@udc.es) 60 | - Maria Isabel Limaylla: [maria.limaylla@udc.es](mailto:maria.limaylla@udc.es) 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all java_parser python_parser js_parser python_prepare_package clean test dev 2 | 3 | all: java_parser python_parser js_parser 4 | 5 | LIB_FLAG = -lib 6 | LIB_PATH = uvl 7 | 8 | ROOT_DIR = $(shell pwd) 9 | PYTHON_OUTPUT_DIR = python/uvl 10 | JAVASCRIPT_OUTPUT_DIR = js/src/lib 11 | 12 | java_parser: 13 | cd java && mvn package 14 | cd java && mvn install 15 | 16 | js_parser: 17 | mkdir -p $(JAVASCRIPT_OUTPUT_DIR) 18 | cd uvl/JavaScript && \ 19 | antlr4 $(LIB_FLAG) $(ROOT_DIR)/$(LIB_PATH) -Dlanguage=JavaScript -o $(ROOT_DIR)/$(JAVASCRIPT_OUTPUT_DIR) UVLJavaScriptLexer.g4 UVLJavaScriptParser.g4 20 | 21 | python_parser: 22 | cd uvl/Python && \ 23 | antlr4 $(LIB_FLAG) $(ROOT_DIR)/$(LIB_PATH) -Dlanguage=Python3 -o $(ROOT_DIR)/$(PYTHON_OUTPUT_DIR) UVLPythonLexer.g4 UVLPythonParser.g4 24 | 25 | python_prepare_package: 26 | cd python && python3 -m build 27 | 28 | clean: 29 | # Clean Java build artifacts 30 | cd java && mvn clean 31 | 32 | # Remove generated Python files except custom_lexer.py and main.py 33 | find python/uvl/ -type f ! -name 'UVLCustomLexer.py' ! -name 'main.py' -delete 34 | # Remove compiled Python files 35 | find . -name "*.pyc" -exec rm {} \; 36 | # Remove Python build artifacts 37 | rm -rf python/build python/dist 38 | 39 | # Clean JavaScript generated files 40 | rm -rf js/src/lib 41 | 42 | 43 | test: 44 | python3 ./python/main.py ./tests/parsing/boolean.uvl 45 | dev: 46 | pip install antlr4-tools antlr4-python3-runtime setuptools wheel twine build 47 | -------------------------------------------------------------------------------- /uvl/UVLLexer.g4: -------------------------------------------------------------------------------- 1 | lexer grammar UVLLexer; 2 | 3 | INDENT: ''; 4 | DEDENT: ''; 5 | 6 | INCLUDE_KEY: 'include'; 7 | FEATURES_KEY: 'features'; 8 | IMPORTS_KEY: 'imports'; 9 | NAMESPACE_KEY: 'namespace'; 10 | AS_KEY: 'as'; 11 | CONSTRAINT_KEY: 'constraint'; 12 | CONSTRAINTS_KEY: 'constraints'; 13 | CARDINALITY_KEY: 'cardinality'; 14 | STRING_KEY: 'String'; 15 | BOOLEAN_KEY: 'Boolean'; 16 | INTEGER_KEY: 'Integer'; 17 | REAL_KEY: 'Real'; 18 | LEN_KEY: 'len'; 19 | SUM_KEY: 'sum'; 20 | AVG_KEY: 'avg'; 21 | FLOOR_KEY: 'floor'; 22 | CEIL_KEY: 'ceil'; 23 | TYPE_KEY: 'Type'; 24 | ARITHMETIC_KEY: 'Arithmetic'; 25 | GROUP_CARDINALITY_KEY: 'group-cardinality'; 26 | FEATURE_CARDINALITY_KEY: 'feature-cardinality'; 27 | AGGREGATE_KEY: 'aggregate-function'; 28 | STRING_CONSTRAINTS_KEY: 'string-constraints'; 29 | 30 | ORGROUP: 'or'; 31 | ALTERNATIVE: 'alternative'; 32 | OPTIONAL: 'optional'; 33 | MANDATORY: 'mandatory'; 34 | 35 | CARDINALITY: '[' INTEGER ('..' (INTEGER | '*'))? ']'; 36 | 37 | NOT: '!'; 38 | AND: '&'; 39 | OR: '|'; 40 | EQUIVALENCE: '<=>'; 41 | IMPLICATION: '=>'; 42 | 43 | EQUAL: '=='; 44 | LOWER: '<'; 45 | LOWER_EQUALS: '<='; 46 | GREATER: '>'; 47 | GREATER_EQUALS: '>='; 48 | NOT_EQUALS: '!='; 49 | 50 | DIV: '/'; 51 | MUL: '*'; 52 | ADD: '+'; 53 | SUB: '-'; 54 | 55 | OPEN_PAREN: '('; 56 | CLOSE_PAREN: ')'; 57 | OPEN_BRACK: '['; 58 | CLOSE_BRACK: ']'; 59 | OPEN_BRACE: '{'; 60 | CLOSE_BRACE: '}'; 61 | OPEN_COMMENT: '/*'; 62 | CLOSE_COMMENT: '*/'; 63 | 64 | FLOAT: '-'?[0-9]*[.][0-9]+; 65 | INTEGER: '0' | '-'?[1-9][0-9]*; 66 | BOOLEAN: 'true' | 'false'; 67 | 68 | COMMA: ','; 69 | DOT: '.'; 70 | 71 | ID_NOT_STRICT: '"'~[\r\n".]+'"'; 72 | ID_STRICT: [a-zA-Z]([a-zA-Z0-9_#§%?\\'äüöß;])*; 73 | 74 | STRING: '\''~[\r\n']+'\''; 75 | 76 | SKIP_: ( SPACES | COMMENT) -> skip; 77 | 78 | fragment COMMENT: 79 | '//' ~[\r\n\f]* 80 | | OPEN_COMMENT .* CLOSE_COMMENT; 81 | fragment SPACES: [ \t]+; 82 | 83 | NEWLINE: ('\r'? '\n' | '\r'); 84 | -------------------------------------------------------------------------------- /python/main.py: -------------------------------------------------------------------------------- 1 | from antlr4 import CommonTokenStream, FileStream 2 | from uvl.UVLCustomLexer import UVLCustomLexer 3 | from uvl.UVLPythonParser import UVLPythonParser 4 | from antlr4.error.ErrorListener import ErrorListener 5 | 6 | class CustomErrorListener(ErrorListener): 7 | def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): 8 | # If the error message contains a specific keyword related to tabulation, ignore it 9 | if "\\t" in msg: 10 | print(f"The UVL has the following warning that prevents reading it :Line {line}:{column} - {msg}") 11 | return 12 | else: 13 | # Otherwise, print the error (or handle it in another way) 14 | raise Exception(f"The UVL has the following error that prevents reading it :Line {line}:{column} - {msg}") 15 | 16 | 17 | def get_tree(argv): 18 | input_stream = FileStream(argv) 19 | lexer = UVLCustomLexer(input_stream) 20 | 21 | # Attach the custom error listener to the lexer 22 | lexer.removeErrorListeners() 23 | lexer.addErrorListener(CustomErrorListener()) 24 | 25 | stream = CommonTokenStream(lexer) 26 | parser = UVLPythonParser(stream) 27 | 28 | # Attach the custom error listener to the parser 29 | parser.removeErrorListeners() 30 | parser.addErrorListener(CustomErrorListener()) 31 | 32 | tree = parser.feature_model() 33 | 34 | return tree 35 | 36 | 37 | if __name__ == "__main__": 38 | import sys 39 | 40 | # Check if the user provided a file argument 41 | if len(sys.argv) < 2: 42 | print("Usage: python script_name.py ") 43 | sys.exit(1) 44 | 45 | # Parse the provided file 46 | input_stream = FileStream(sys.argv[1]) 47 | lexer = UVLCustomLexer(input_stream) 48 | 49 | # Attach the custom error listener to the lexer 50 | lexer.removeErrorListeners() 51 | lexer.addErrorListener(CustomErrorListener()) 52 | 53 | stream = CommonTokenStream(lexer) 54 | parser = UVLPythonParser(stream) 55 | 56 | # Attach the custom error listener to the parser 57 | parser.removeErrorListeners() 58 | parser.addErrorListener(CustomErrorListener()) 59 | 60 | tree = parser.featureModel() 61 | # Print the parse tree (optional) 62 | print(tree.toStringTree(recog=parser)) -------------------------------------------------------------------------------- /python/uvl/UVLCustomLexer.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .UVLPythonLexer import UVLPythonLexer 3 | 4 | from .UVLPythonParser import UVLPythonParser 5 | from antlr4.Token import Token 6 | from antlr4.Token import CommonToken 7 | 8 | class UVLCustomLexer(UVLPythonLexer): 9 | 10 | def __init__(self, input_stream): 11 | super().__init__(input_stream) 12 | self.tokens = [] 13 | self.indents = [] 14 | self.opened = 0 15 | self.lastToken = None 16 | 17 | def emitToken(self, t): 18 | super().emitToken(t) 19 | self.tokens.append(t) 20 | 21 | def nextToken(self): 22 | # Check if the end-of-file is ahead and there are still some DEDENTS expected. 23 | if self._input.LA(1) == Token.EOF and len(self.indents) != 0: 24 | # Remove any trailing EOF tokens from our buffer. 25 | while len(self.tokens) > 0 and self.tokens[-1].type == Token.EOF: 26 | del self.tokens[-1] 27 | 28 | # First emit an extra line break that serves as the end of the statement. 29 | self.emitToken(self.common_token(UVLPythonLexer.NEWLINE, "\n")); 30 | 31 | 32 | # Now emit as much DEDENT tokens as needed. 33 | while len(self.indents) != 0: 34 | self.emitToken(self.create_dedent()) 35 | del self.indents[-1] 36 | # Put the EOF back on the token stream. 37 | self.emitToken(self.common_token(Token.EOF, "")); 38 | 39 | next_token = super().nextToken() 40 | 41 | if next_token.channel == Token.DEFAULT_CHANNEL: 42 | # Keep track of the last token on the default channel. 43 | self.lastToken = next_token 44 | 45 | return self.tokens.pop(0) if len(self.tokens) > 0 else next_token 46 | 47 | def create_dedent(self): 48 | dedent = self.common_token(self.DEDENT, "") 49 | dedent.line = self.lastToken.line 50 | return dedent 51 | 52 | def common_token(self, type, text): 53 | stop = self.getCharIndex() - 1 54 | start = stop - len(text) + 1 if text else stop 55 | return CommonToken(self._tokenFactorySourcePair, type, Token.DEFAULT_CHANNEL, start, stop) 56 | 57 | @staticmethod 58 | def getIndentationCount(spaces): 59 | count = 0 60 | for ch in spaces: 61 | if ch == '\t': 62 | count += 8 - (count % 8) 63 | else: # A normal space char. 64 | count += 1 65 | return count 66 | 67 | def skipToken(self): 68 | self.skip() 69 | 70 | def atStartOfInput(self): 71 | return self._interp.column == 0 and self._interp.line == 1 72 | 73 | def handleNewline(self): 74 | new_line = re.sub(r"[^\r\n\f]+", "", self._interp.getText(self._input)) #.replaceAll("[^\r\n\f]+", "") 75 | spaces = re.sub(r"[\r\n\f]+", "", self._interp.getText(self._input)) #.replaceAll("[\r\n\f]+", "") 76 | next = self._input.LA(1) 77 | 78 | if self.opened > 0 or next == '\r' or next == '\n' or next == '\f' or next == '#': 79 | self.skip() 80 | else: 81 | self.emitToken(self.common_token(self.NEWLINE, new_line)) 82 | 83 | indent = self.getIndentationCount(spaces) 84 | if len(self.indents) == 0: 85 | previous = 0 86 | else: 87 | previous = self.indents[-1] 88 | 89 | if indent == previous: 90 | self.skip() 91 | elif indent > previous: 92 | self.indents.append(indent) 93 | self.emitToken(self.common_token(UVLPythonParser.INDENT, spaces)) 94 | else: 95 | while len(self.indents) > 0 and self.indents[-1] > indent: 96 | self.emitToken(self.create_dedent()) 97 | del self.indents[-1] -------------------------------------------------------------------------------- /js/src/UVLJavaScriptCustomLexer.js: -------------------------------------------------------------------------------- 1 | import UVLJavaScriptLexer from './lib/UVLJavaScriptLexer.js'; 2 | import UVLJavaScriptParser from './lib/UVLJavaScriptParser.js'; 3 | import antlr4 from 'antlr4'; 4 | 5 | export default class UVLJavaScriptCustomLexer extends UVLJavaScriptLexer { 6 | 7 | constructor(input_stream) { 8 | super(input_stream); 9 | this.tokens = []; 10 | this.indents = []; 11 | this.opened = 0; 12 | this.lastToken = null; 13 | } 14 | 15 | emitToken(t) { 16 | super.emitToken(t); 17 | this.tokens.push(t); 18 | } 19 | 20 | nextToken() { 21 | if (this._input.LA(1) === antlr4.Token.EOF && this.indents.length !== 0) { 22 | while (this.tokens.length > 0 && this.tokens[this.tokens.length - 1].type === antlr4.Token.EOF) { 23 | this.tokens.pop(); 24 | } 25 | 26 | this.emitToken(this.commonToken(UVLJavaScriptLexer.NEWLINE, "\n")); 27 | 28 | while (this.indents.length !== 0) { 29 | this.emitToken(this.createDedent()); 30 | this.indents.pop(); 31 | } 32 | 33 | this.emitToken(this.commonToken(antlr4.Token.EOF, "")); 34 | } 35 | 36 | const nextToken = super.nextToken(); 37 | 38 | if (nextToken.channel === antlr4.Token.DEFAULT_CHANNEL) { 39 | this.lastToken = nextToken; 40 | } 41 | 42 | return this.tokens.length > 0 ? this.tokens.shift() : nextToken; 43 | } 44 | 45 | createDedent() { 46 | const dedent = this.commonToken(UVLJavaScriptLexer.DEDENT, ""); 47 | dedent.line = this.lastToken.line; 48 | return dedent; 49 | } 50 | 51 | commonToken(type, text) { 52 | const stop = this.getCharIndex() - 1; 53 | const start = text ? stop - text.length + 1 : stop; 54 | return new antlr4.CommonToken(this._tokenFactorySourcePair, type, antlr4.Token.DEFAULT_CHANNEL, start, stop); 55 | } 56 | 57 | static getIndentationCount(spaces) { 58 | let count = 0; 59 | for (const ch of spaces) { 60 | if (ch === '\t') { 61 | count += 8 - (count % 8); 62 | } else { 63 | count += 1; 64 | } 65 | } 66 | return count; 67 | } 68 | 69 | skipToken() { 70 | this.skip(); 71 | } 72 | 73 | atStartOfInput() { 74 | return this._interp.column === 0 && this._interp.line === 1; 75 | } 76 | 77 | handleNewline() { 78 | const newLine = this._interp.getText(this._input).replace(/[^\r\n\f]+/g, ""); 79 | const spaces = this._interp.getText(this._input).replace(/[\r\n\f]+/g, ""); 80 | const next = String.fromCharCode(this._input.LA(1)); 81 | 82 | if (this.opened > 0 || next === '\r' || next === '\n' || next === '\f' || next === '#') { 83 | this.skip(); 84 | } else { 85 | this.emitToken(this.commonToken(UVLJavaScriptLexer.NEWLINE, newLine)); 86 | 87 | const indent = UVLJavaScriptCustomLexer.getIndentationCount(spaces); 88 | const previous = this.indents.length === 0 ? 0 : this.indents[this.indents.length - 1]; 89 | 90 | if (indent === previous) { 91 | this.skip(); 92 | } else if (indent > previous) { 93 | this.indents.push(indent); 94 | this.emitToken(this.commonToken(UVLJavaScriptParser.INDENT, spaces)); 95 | } else { 96 | while (this.indents.length > 0 && this.indents[this.indents.length - 1] > indent) { 97 | this.emitToken(this.createDedent()); 98 | this.indents.pop(); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /uvl/UVLParser.g4: -------------------------------------------------------------------------------- 1 | parser grammar UVLParser; 2 | 3 | options { 4 | tokenVocab = UVLLexer; 5 | } 6 | 7 | featureModel: 8 | namespace? NEWLINE? includes? NEWLINE? imports? NEWLINE? features? NEWLINE? constraints? EOF; 9 | 10 | includes: INCLUDE_KEY NEWLINE INDENT includeLine* DEDENT; 11 | includeLine: languageLevel NEWLINE; 12 | 13 | namespace: NAMESPACE_KEY reference; 14 | 15 | imports: IMPORTS_KEY NEWLINE INDENT importLine* DEDENT; 16 | importLine: ns = reference (AS_KEY alias = reference)? NEWLINE; 17 | 18 | features: FEATURES_KEY NEWLINE INDENT feature DEDENT; 19 | 20 | group: 21 | ORGROUP groupSpec # OrGroup 22 | | ALTERNATIVE groupSpec # AlternativeGroup 23 | | OPTIONAL groupSpec # OptionalGroup 24 | | MANDATORY groupSpec # MandatoryGroup 25 | | CARDINALITY groupSpec # CardinalityGroup; 26 | 27 | groupSpec: NEWLINE INDENT feature+ DEDENT; 28 | 29 | feature: 30 | featureType? reference featureCardinality? attributes? NEWLINE ( 31 | INDENT group+ DEDENT 32 | )?; 33 | 34 | featureCardinality: CARDINALITY_KEY CARDINALITY; 35 | 36 | attributes: 37 | OPEN_BRACE (attribute (COMMA attribute)*)? CLOSE_BRACE; 38 | 39 | attribute: valueAttribute | constraintAttribute; 40 | 41 | valueAttribute: key value?; 42 | key: id; 43 | value: BOOLEAN | FLOAT | INTEGER | STRING | attributes | vector; 44 | vector: OPEN_BRACK (value (COMMA value)*)? CLOSE_BRACK; 45 | 46 | constraintAttribute: 47 | CONSTRAINT_KEY constraint # SingleConstraintAttribute 48 | | CONSTRAINTS_KEY constraintList # ListConstraintAttribute; 49 | constraintList: 50 | OPEN_BRACK (constraint (COMMA constraint)*)? CLOSE_BRACK; 51 | 52 | constraints: 53 | CONSTRAINTS_KEY NEWLINE INDENT constraintLine* DEDENT; 54 | 55 | constraintLine: constraint NEWLINE; 56 | 57 | constraint: 58 | equation # EquationConstraint 59 | | reference # LiteralConstraint 60 | | OPEN_PAREN constraint CLOSE_PAREN # ParenthesisConstraint 61 | | NOT constraint # NotConstraint 62 | | constraint AND constraint # AndConstraint 63 | | constraint OR constraint # OrConstraint 64 | | constraint IMPLICATION constraint # ImplicationConstraint 65 | | constraint EQUIVALENCE constraint # EquivalenceConstraint; 66 | 67 | equation: 68 | expression EQUAL expression # EqualEquation 69 | | expression LOWER expression # LowerEquation 70 | | expression GREATER expression # GreaterEquation 71 | | expression LOWER_EQUALS expression # LowerEqualsEquation 72 | | expression GREATER_EQUALS expression # GreaterEqualsEquation 73 | | expression NOT_EQUALS expression # NotEqualsEquation; 74 | 75 | expression 76 | : additiveExpression 77 | ; 78 | 79 | additiveExpression 80 | : additiveExpression ADD multiplicativeExpression # AddExpression 81 | | additiveExpression SUB multiplicativeExpression # SubExpression 82 | | multiplicativeExpression # MultiplicativeExpr 83 | ; 84 | 85 | multiplicativeExpression 86 | : multiplicativeExpression MUL primaryExpression # MulExpression 87 | | multiplicativeExpression DIV primaryExpression # DivExpression 88 | | primaryExpression # PrimaryExpressionExpression 89 | ; 90 | 91 | primaryExpression 92 | : FLOAT # FloatLiteralExpression 93 | | INTEGER # IntegerLiteralExpression 94 | | STRING # StringLiteralExpression 95 | | aggregateFunction # AggregateFunctionExpression 96 | | reference # LiteralExpression 97 | | OPEN_PAREN expression CLOSE_PAREN # BracketExpression 98 | ; 99 | 100 | aggregateFunction: 101 | sumAggregateFunction # SumAggregateFunctionExpression 102 | | avgAggregateFunction # AvgAggregateFunctionExpression 103 | | stringAggregateFunction # StringAggregateFunctionExpression 104 | | numericAggregateFunction # NumericAggregateFunctionExpression 105 | ; 106 | 107 | sumAggregateFunction: 108 | SUM_KEY OPEN_PAREN (reference COMMA)? reference CLOSE_PAREN; 109 | 110 | 111 | avgAggregateFunction: 112 | AVG_KEY OPEN_PAREN (reference COMMA)? reference CLOSE_PAREN; 113 | 114 | stringAggregateFunction: 115 | LEN_KEY OPEN_PAREN reference CLOSE_PAREN # LengthAggregateFunction 116 | ; 117 | 118 | numericAggregateFunction: 119 | FLOOR_KEY OPEN_PAREN reference CLOSE_PAREN # FloorAggregateFunction 120 | | CEIL_KEY OPEN_PAREN reference CLOSE_PAREN # CeilAggregateFunction 121 | ; 122 | 123 | reference: (id DOT)* id; 124 | id: ID_STRICT | ID_NOT_STRICT; 125 | 126 | featureType: STRING_KEY | INTEGER_KEY | BOOLEAN_KEY | REAL_KEY; 127 | 128 | languageLevel: majorLevel (DOT (minorLevel | MUL))?; 129 | 130 | majorLevel: BOOLEAN_KEY | ARITHMETIC_KEY | TYPE_KEY; 131 | 132 | minorLevel: 133 | GROUP_CARDINALITY_KEY 134 | | FEATURE_CARDINALITY_KEY 135 | | AGGREGATE_KEY 136 | | STRING_CONSTRAINTS_KEY; 137 | -------------------------------------------------------------------------------- /uvl/Java/UVLJavaLexer.g4: -------------------------------------------------------------------------------- 1 | lexer grammar UVLJavaLexer; 2 | 3 | import UVLLexer; 4 | 5 | @header { 6 | package uvl; 7 | } 8 | 9 | @members { 10 | // A queue where extra tokens are pushed on (see the NEWLINE lexer rule). 11 | private java.util.LinkedList tokens = new java.util.LinkedList<>(); 12 | // The stack that keeps track of the indentation level. 13 | private java.util.Stack indents = new java.util.Stack<>(); 14 | // The amount of opened braces, brackets and parenthesis. 15 | private int opened = 0; 16 | // The most recently produced token. 17 | private Token lastToken = null; 18 | 19 | @Override 20 | public void emit(Token t) { 21 | super.setToken(t); 22 | tokens.offer(t); 23 | } 24 | 25 | @Override 26 | public Token nextToken() { 27 | // Check if the end-of-file is ahead and there are still some DEDENTS expected. 28 | if (_input.LA(1) == EOF && !this.indents.isEmpty()) { 29 | // Remove any trailing EOF tokens from our buffer. 30 | for (int i = tokens.size() - 1; i >= 0; i--) { 31 | if (tokens.get(i).getType() == EOF) { 32 | tokens.remove(i); 33 | } 34 | } 35 | // First emit an extra line break that serves as the end of the statement. 36 | this.emit(commonToken(UVLJavaParser.NEWLINE, "\n")); 37 | // Now emit as much DEDENT tokens as needed. 38 | while (!indents.isEmpty()) { 39 | this.emit(createDedent()); 40 | indents.pop(); 41 | } 42 | // Put the EOF back on the token stream. 43 | this.emit(commonToken(UVLJavaParser.EOF, "")); 44 | } 45 | 46 | Token next = super.nextToken(); 47 | if (next.getChannel() == Token.DEFAULT_CHANNEL) { 48 | // Keep track of the last token on the default channel. 49 | this.lastToken = next; 50 | } 51 | 52 | return tokens.isEmpty() ? next : tokens.poll(); 53 | } 54 | 55 | private Token createDedent() { 56 | CommonToken dedent = commonToken(UVLJavaParser.DEDENT, ""); 57 | dedent.setLine(this.lastToken.getLine()); 58 | return dedent; 59 | } 60 | 61 | private CommonToken commonToken(int type, String text) { 62 | int stop = this.getCharIndex() - 1; 63 | int start = text.isEmpty() ? stop : stop - text.length() + 1; 64 | return new CommonToken(this._tokenFactorySourcePair, type, DEFAULT_TOKEN_CHANNEL, start, stop); 65 | } 66 | 67 | // Calculates the indentation of the provided spaces, taking the 68 | // following rules into account: 69 | // 70 | // "Tabs are replaced (from left to right) by one to eight spaces 71 | // such that the total number of characters up to and including 72 | // the replacement is a multiple of eight [...]" 73 | // 74 | // -- https://docs.python.org/3.1/reference/lexical_analysis.html#indentation 75 | static int getIndentationCount(String spaces) { 76 | int count = 0; 77 | for (char ch : spaces.toCharArray()) { 78 | switch (ch) { 79 | case '\t': 80 | count += 8 - (count % 8); 81 | break; 82 | default: 83 | // A normal space char. 84 | count++; 85 | } 86 | } 87 | return count; 88 | } 89 | 90 | boolean atStartOfInput() { 91 | return super.getCharPositionInLine() == 0 && super.getLine() == 1; 92 | } 93 | } 94 | 95 | // Indentation-sensitive tokens 96 | OPEN_PAREN: '(' {this.opened += 1;}; 97 | CLOSE_PAREN: ')' {this.opened -= 1;}; 98 | OPEN_BRACK: '[' {this.opened += 1;}; 99 | CLOSE_BRACK: ']' {this.opened -= 1;}; 100 | OPEN_BRACE: '{' {this.opened += 1;}; 101 | CLOSE_BRACE: '}' {this.opened -= 1;}; 102 | OPEN_COMMENT: '/*' {this.opened += 1;}; 103 | CLOSE_COMMENT: '*/' {this.opened -= 1;}; 104 | 105 | // Indentation-sensitive NEWLINE rule 106 | NEWLINE: ( 107 | {atStartOfInput()}? SPACES 108 | | ( '\r'? '\n' | '\r') SPACES? 109 | ) { 110 | String newLine = getText().replaceAll("[^\r\n]+", ""); 111 | String spaces = getText().replaceAll("[\r\n]+", ""); 112 | int next = _input.LA(1); 113 | int nextNext = _input.LA(2); 114 | 115 | if (opened > 0 || next == '\r' || next == '\n' || (next == '/' && nextNext == '/')) { 116 | // If we're inside a list or on a blank line, ignore all indents, 117 | // dedents and line breaks. 118 | skip(); 119 | } else { 120 | emit(commonToken(UVLJavaParser.NEWLINE, newLine)); 121 | int indent = getIndentationCount(spaces); 122 | int previous = indents.isEmpty() ? 0 : indents.peek(); 123 | if (indent == previous) { 124 | // skip indents of the same size as the present indent-size 125 | skip(); 126 | } 127 | else if (indent > previous) { 128 | indents.push(indent); 129 | emit(commonToken(UVLJavaParser.INDENT, spaces)); 130 | } 131 | else { 132 | // Possibly emit more than 1 DEDENT token. 133 | while(!indents.isEmpty() && indents.peek() > indent) { 134 | this.emit(createDedent()); 135 | indents.pop(); 136 | } 137 | } 138 | } 139 | }; 140 | 141 | // Fragment used by NEWLINE 142 | fragment SPACES: [ \t]+; 143 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # UVL - Universal Variability Language 2 | 3 | **UVL (Universal Variability Language)** is a concise and extensible language for modeling variability in software product lines. It supports multiple programming languages and provides a grammar-based foundation for building tools and parsers. 4 | 5 | This repository contains the **ANTLR4 grammar files** for UVL. With these, you can generate parsers for UVL tailored to specific programming languages like Java, JavaScript, and Python. 6 | 7 | ## ✨ Key Features 8 | 9 | - Language-level modularity 10 | - Namespaces and imports 11 | - Feature trees with attributes and cardinalities 12 | - Cross-tree constraints 13 | - Extensible for different target languages 14 | 15 | ## 📦 Repository Structure 16 | 17 | - `uvl/UVLParser.g4` – Base grammar in EBNF form 18 | - `uvl/UVLLexer.g4` – Base lexer grammar for UVL 19 | - `uvl/Java/UVLJava*.g4`, `uvl/Python/UVLPython*.g4`, etc. – Language-specific grammar files 20 | - `java/` – Java-based parser implementation using Maven 21 | - `python/` – Python-based parser implementation 22 | - `javascript/` – JavaScript-based parser implementation 23 | - `tests/` – UVL test cases for validation 24 | 25 | UVL uses [ANTLR4](https://www.antlr.org/) as its parser generator. 26 | 27 | --- 28 | 29 | ## 💡 Language Overview 30 | 31 | Each UVL model may consist of five optional sections: 32 | 33 | 1. **Language levels**: Enable optional concepts via `include` keyword. 34 | 2. **Namespace**: Allows referencing the model from other UVL models. 35 | 3. **Imports**: Include other feature models (e.g., `subdir.filename as fn`). 36 | 4. **Feature tree**: Hierarchical features with cardinalities, attributes, and group types (`mandatory`, `optional`, `or`, `alternative`). 37 | 5. **Cross-tree constraints**: Logical and arithmetic constraints among features. 38 | 39 | ### 🔍 Example 40 | 41 | ```uvl 42 | namespace Server 43 | 44 | features 45 | Server {abstract} 46 | mandatory 47 | FileSystem 48 | or 49 | NTFS 50 | APFS 51 | EXT4 52 | OperatingSystem {abstract} 53 | alternative 54 | Windows 55 | macOS 56 | Debian 57 | optional 58 | Logging { 59 | default, 60 | log_level "warn" 61 | } 62 | 63 | constraints 64 | Windows => NTFS 65 | macOS => APFS 66 | ``` 67 | 68 | **Explanation:** 69 | - `Server` is an abstract feature. 70 | - It must include a `FileSystem` and an `OperatingSystem`. 71 | - `Logging` is optional and includes an attribute. 72 | - Logical constraints define dependencies between features. 73 | 74 | 🔗 More examples: https://github.com/Universal-Variability-Language/uvl-models/tree/main/Feature_Models 75 | 76 | --- 77 | 78 | 79 | ## Usage 80 | To use UVL in your projects, you can either: 81 | 1. **Use the pre-built parsers** 82 | ### Java Parser 83 | Include the following dependency in your Maven project: 84 | ```xml 85 | 86 | io.github.universal-variability-language 87 | uvl-parser 88 | 0.3 89 | 90 | ``` 91 | ### Python Parser 92 | Install the package via pip: 93 | ```bash 94 | pip install uvlparser 95 | ``` 96 | ### JavaScript Parser 97 | Install the package via npm: 98 | ```bash 99 | npm install uvl-parser 100 | ``` 101 | 2. **Build the parser manually** See the sections below for details. 102 | 103 | ## ⚙️ Building the Parser manually 104 | ### Java Parser 105 | #### Prerequisites 106 | 107 | - [ANTLR4](https://www.antlr.org/) 108 | - Java 17+ 109 | - [Maven](https://maven.apache.org/) 110 | 111 | #### Build Steps 112 | 113 | 1. Clone the repository: 114 | ```bash 115 | git clone https://github.com/Universal-Variability-Language/uvl-parser 116 | ``` 117 | 118 | 2. Build the parser: 119 | ```bash 120 | cd java 121 | mvn clean package 122 | ``` 123 | 124 | 3. Include the generated JAR in your Java project. 125 | --- 126 | 127 | ## 📚 Resources 128 | 129 | **UVL Models & Tools** 130 | - https://github.com/Universal-Variability-Language/uvl-models 131 | - https://www.uvlhub.io/ 132 | 133 | **Tooling Ecosystem** 134 | - https://github.com/FeatureIDE/FeatureIDE 135 | - https://ide.flamapy.org/ 136 | - https://github.com/Universal-Variability-Language/uvl-lsp 137 | - https://github.com/SECPS/TraVarT 138 | - https://github.com/AlexCortinas/spl-js-engine 139 | 140 | --- 141 | 142 | ## 📖 Citation 143 | 144 | If you use UVL in your research, please cite: 145 | 146 | ```bibtex 147 | @article{UVL2024, 148 | title = {UVL: Feature modelling with the Universal Variability Language}, 149 | journal = {Journal of Systems and Software}, 150 | volume = {225}, 151 | pages = {112326}, 152 | year = {2025}, 153 | issn = {0164-1212}, 154 | doi = {https://doi.org/10.1016/j.jss.2024.112326}, 155 | url = {https://www.sciencedirect.com/science/article/pii/S0164121224003704}, 156 | author = {David Benavides and Chico Sundermann and Kevin Feichtinger and José A. Galindo and Rick Rabiser and Thomas Thüm}, 157 | keywords = {Feature model, Software product lines, Variability} 158 | } 159 | ``` 160 | --- 161 | 162 | ## 📬 Contact & Contributions 163 | 164 | Feel free to open issues or pull requests if you have suggestions or improvements. For questions or collaboration inquiries, visit the UVL Website: 165 | https://universal-variability-language.github.io/ 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UVL - Universal Variability Language 2 | 3 | **UVL (Universal Variability Language)** is a concise and extensible language for modeling variability in software product lines. It supports multiple programming languages and provides a grammar-based foundation for building tools and parsers. 4 | 5 | This repository contains the **ANTLR4 grammar files** for UVL. With these, you can generate parsers for UVL tailored to specific programming languages like Java, JavaScript, and Python. 6 | 7 | ## ✨ Key Features 8 | 9 | - Language-level modularity 10 | - Namespaces and imports 11 | - Feature trees with attributes and cardinalities 12 | - Cross-tree constraints 13 | - Extensible for different target languages 14 | 15 | ## 📦 Repository Structure 16 | 17 | - `uvl/UVLParser.g4` – Base grammar in EBNF form 18 | - `uvl/UVLLexer.g4` – Base lexer grammar for UVL 19 | - `uvl/Java/UVLJava*.g4`, `uvl/Python/UVLPython*.g4`, etc. – Language-specific grammar files 20 | - `java/` – Java-based parser implementation using Maven (generated) 21 | - `python/` – Python-based parser implementation (generated) 22 | - `js/` – JavaScript-based parser implementation (generated) 23 | 24 | UVL uses [ANTLR4](https://www.antlr.org/) as its parser generator. 25 | 26 | ## Usage 27 | 28 | To use UVL in your projects, you can either: 29 | 30 | 1. **Use the pre-built parsers** 31 | ### Java Parser 32 | Include the following dependency in your Maven project: 33 | ```xml 34 | 35 | io.github.universal-variability-language 36 | uvl-parser 37 | 0.3 38 | 39 | ``` 40 | ### Python Parser 41 | Install the package via pip: 42 | ```bash 43 | pip install uvlparser 44 | ``` 45 | ### JavaScript Parser 46 | Install the package via npm: 47 | ```bash 48 | npm install uvl-parser 49 | ``` 50 | 2. **Build the parser manually** See the sections below for details. 51 | 52 | ## ⚙️ Building the Parser manually 53 | 54 | ### Java Parser 55 | 56 | #### Prerequisites 57 | 58 | - Java 17+ 59 | - [Maven](https://maven.apache.org/) 60 | 61 | #### Build Steps 62 | 63 | 1. Clone the repository: 64 | 65 | ```bash 66 | git clone https://github.com/Universal-Variability-Language/uvl-parser 67 | ``` 68 | 69 | 2. Build the parser: 70 | 71 | ```bash 72 | make java_parser 73 | ``` 74 | 75 | This will generate the jar file in the `java/target/` directory. You can also build the JAR with: 76 | 77 | ```bash 78 | cd java && mvn clean package 79 | ``` 80 | 81 | 3. Include the generated JAR in your Java project. 82 | 83 | --- 84 | 85 | ### Python Parser 86 | 87 | #### Prerequisites 88 | 89 | - Python 3.8+ 90 | - [ANTLR4](https://www.antlr.org/) 91 | 92 | #### Build Steps 93 | 94 | 1. Clone the repository: 95 | 96 | ```bash 97 | git clone https://github.com/Universal-Variability-Language/uvl-parser 98 | ``` 99 | 100 | 2. Build the parser: 101 | 102 | ```bash 103 | make python_parser 104 | ``` 105 | 106 | This will generate the parser files in the `python/` directory. To build the wheel package, run: 107 | 108 | ```bash 109 | make python_prepare_package 110 | ``` 111 | 112 | ### JavaScript Parser 113 | 114 | #### Prerequisites 115 | 116 | - Node.js 14+ 117 | - [ANTLR4](https://www.antlr.org/) 118 | 119 | #### Build Steps 120 | 121 | 1. Clone the repository: 122 | 123 | ```bash 124 | git clone https://github.com/Universal-Variability-Language/uvl-parser 125 | ``` 126 | 127 | 2. Build the parser: 128 | 129 | ```bash 130 | make javascript_parser 131 | ``` 132 | 133 | ## This will generate the parser files in the `js/` directory. 134 | 135 | ## 💡 Universal-Variability-Language (UVL) 136 | 137 | For comprehensive guidance on utilizing UVL, please refer to the following publication: 138 | 139 | [![DOI](https://img.shields.io/badge/DOI-10.1016%2Fj.jss.2024.112326-blue)](https://doi.org/10.1016/j.jss.2024.112326) 140 | 141 | 🔗 **Sample UVL models** are available at: https://github.com/Universal-Variability-Language/uvl-models 142 | 143 | --- 144 | 145 | ## 📚 Resources 146 | 147 | **UVL Models & Tools** 148 | 149 | - https://github.com/Universal-Variability-Language/uvl-models 150 | - https://www.uvlhub.io/ 151 | 152 | **Tooling Ecosystem** 153 | 154 | - https://github.com/FeatureIDE/FeatureIDE 155 | - https://ide.flamapy.org/ 156 | - https://github.com/Universal-Variability-Language/uvl-lsp 157 | - https://github.com/SECPS/TraVarT 158 | - https://github.com/AlexCortinas/spl-js-engine 159 | 160 | --- 161 | 162 | ## 📖 Citation 163 | 164 | If you use UVL in your research, please cite: 165 | 166 | ```bibtex 167 | @article{UVL2024, 168 | title = {UVL: Feature modelling with the Universal Variability Language}, 169 | journal = {Journal of Systems and Software}, 170 | volume = {225}, 171 | pages = {112326}, 172 | year = {2025}, 173 | issn = {0164-1212}, 174 | doi = {https://doi.org/10.1016/j.jss.2024.112326}, 175 | url = {https://www.sciencedirect.com/science/article/pii/S0164121224003704}, 176 | author = {David Benavides and Chico Sundermann and Kevin Feichtinger and José A. Galindo and Rick Rabiser and Thomas Thüm}, 177 | keywords = {Feature model, Software product lines, Variability} 178 | } 179 | ``` 180 | 181 | --- 182 | 183 | ## 📬 Contact & Contributions 184 | 185 | Feel free to open issues or pull requests if you have suggestions or improvements. For questions or collaboration inquiries, visit the UVL Website: 186 | https://universal-variability-language.github.io/ 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | UVL Parser 9 | This project host the grammar files and generated source files for the Java version of the UVL parser 10 | https://github.com/Universal-Variability-Language/uvl-parser 11 | 12 | io.github.universal-variability-language 13 | uvl-parser 14 | 0.4.1 15 | 16 | 17 | 18 | MIT License 19 | https://github.com/Universal-Variability-Language/uvl-parser/blob/master/LICENSE 20 | repo 21 | 22 | 23 | 24 | 25 | 26 | José A. Galindo 27 | jagalindo@us.es 28 | University of Seville 29 | https://www.us.es 30 | 31 | 32 | Chico Sundermann 33 | jagalindo@us.es 34 | University of Ulm 35 | https://www.uni-ulm.de/ 36 | 37 | 38 | Kevin Feichtinger 39 | kevin.feichtinger@jku.at 40 | Johannes Kepler University 41 | https://www.jku.at/ 42 | 43 | 44 | Malte Grave 45 | malte.grave@jku.at 46 | Johannes Kepler University 47 | https://www.jku.at/ 48 | 49 | 50 | 51 | 52 | scm:git:git@github.com:Universal-Variability-Language/uvl-parser.git 53 | scm:git:git@github.com:Universal-Variability-Language/uvl-parser.git 54 | https://github.com/Universal-Variability-Language/uvl-parser 55 | 56 | 57 | 58 | 4.13.2 59 | 21 60 | 21 61 | UTF-8 62 | UTF-8 63 | 64 | 65 | 66 | 67 | org.antlr 68 | antlr4-runtime 69 | ${antlr4.version} 70 | 71 | 72 | 73 | 74 | 75 | ossrh 76 | https://s01.oss.sonatype.org/content/repositories/snapshots 77 | 78 | 79 | ossrh 80 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 81 | 82 | 83 | 84 | 85 | 87 | 88 | 89 | 90 | org.antlr 91 | antlr4-maven-plugin 92 | ${antlr4.version} 93 | 94 | 95 | -lib 96 | ../uvl 97 | 98 | ../uvl/Java 99 | ${project.build.directory}/generated-sources/antlr4 100 | true 101 | true 102 | 103 | 104 | 105 | 106 | antlr4 107 | 108 | generate-sources 109 | 110 | 111 | 112 | 113 | 114 | org.codehaus.mojo 115 | build-helper-maven-plugin 116 | 3.2.0 117 | 118 | 119 | add-source 120 | generate-sources 121 | 122 | add-source 123 | 124 | 125 | 126 | ${project.build.directory}/generated-sources/antlr4 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-compiler-plugin 136 | 3.14.0 137 | 138 | ${maven.compiler.source} 139 | ${maven.compiler.target} 140 | 141 | 142 | 143 | 144 | org.apache.maven.plugins 145 | maven-gpg-plugin 146 | 3.2.7 147 | 148 | 149 | sign-artifacts 150 | verify 151 | 152 | sign 153 | 154 | 155 | 156 | 157 | 158 | 159 | org.sonatype.plugins 160 | nexus-staging-maven-plugin 161 | 1.7.0 162 | true 163 | 164 | ossrh 165 | https://s01.oss.sonatype.org/ 166 | true 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-source-plugin 173 | 3.3.1 174 | 175 | 176 | attach-sources 177 | 178 | jar-no-fork 179 | 180 | 181 | 182 | 183 | 184 | org.apache.maven.plugins 185 | maven-javadoc-plugin 186 | 3.11.2 187 | 188 | 189 | attach-javadocs 190 | 191 | jar 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | --------------------------------------------------------------------------------