diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..4843871bd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb3ce2fa1..b8d6723a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Lint markdown files + uses: DavidAnson/markdownlint-cli2-action@v16 + with: + globs: '**/*.md' + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/.gitignore b/.gitignore index 41be24d79..63798fa65 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ venv/ /test/test_json/test-data.xml /test/test_xsd/*.xsd /test/test_*/out/ +# created by vscode specific scripts: +.env diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..795a6ef89 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,45 @@ +{ + "code-fence-style": { + "style": "backtick" + }, + "fenced-code-language": { + "allowed_languages": [ + "sh", + "json", + "lisp", + "ini", + "text" + ], + "language_only": true + }, + "heading-style": { + "style": "atx" + }, + "hr-style": { + "style": "---" + }, + "line-length": { + "strict": true, + "code_blocks": false + }, + "link-image-style": { + "collapsed": false, + "shortcut": false, + "url_inline": false + }, + "no-duplicate-heading": { + "siblings_only": true + }, + "ol-prefix": { + "style": "ordered" + }, + "reference-links-images": { + "shortcut_syntax": true + }, + "strong-style": { + "style": "asterisk" + }, + "MD013": { + "line_length": 160 + } +} diff --git a/.vscode/create_env.sh b/.vscode/create_env.sh new file mode 100755 index 000000000..e8321e67b --- /dev/null +++ b/.vscode/create_env.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# This script creates .env file at the root of the repository corresponding to +# the environment created when sourcing env.sh at the root of the repository +# It is expected that this script is run from the root of the repository + +#shellcheck source=../env.sh +. ./env.sh + +{ + echo "PATH=\"$PATH\"" + echo "MANPATH=\"$MANPATH\"" + echo "PYTHONPATH=\"$PYTHONPATH\"" + echo "YANG_MODPATH=\"$YANG_MODPATH\"" + echo "PYANG_XSLT_DIR=\"$PYANG_XSLT_DIR\"" + echo "PYANG_RNG_LIBDIR=\"$PYANG_RNG_LIBDIR\"" + echo "W=\"$W\"" +} > ./.env diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..d9ccc4d09 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,21 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "editorconfig.editorconfig", + "ms-python.python", + "ms-python.debugpy", + "ms-python.vscode-pylance", + "kevinrose.vsc-python-indent", + "mads-hartmann.bash-ide-vscode", + "github.vscode-pull-request-github", + "github.vscode-github-actions", + "bierner.github-markdown-preview", + "davidanson.vscode-markdownlint" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..939253920 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,55 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: lsp server", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/bin/pyang", + "console": "integratedTerminal", + "preLaunchTask": "create_env", + "justMyCode": false, + "args": [ + "--lsp", + "--lsp-mode=tcp", + "--lint", + "--strict", + "--canonical", + "--max-line-length=80", + "--max-identifier-length=32", + "--verbose", + "--no-env-path", + "--path=${workspaceFolder}/modules", + ] + }, + { + "name": "Python Debugger: lsp client", + "type": "debugpy", + "request": "launch", + "cwd": "${workspaceFolder}/test/test_lsp", + "program": "client.py", + "console": "integratedTerminal", + "preLaunchTask": "create_env", + "justMyCode": false, + "args": [] + }, + { + "name": "Python Debugger: Remote Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..19dd7de25 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.typeCheckingMode": "basic" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..2cf45d7a3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "create_env", + "type": "shell", + "command": "${workspaceFolder}/.vscode/create_env.sh" + }, + { + "label": "test", + "type": "shell", + "command": ". ./env.sh && make test" + }, + { + "label": "test lsp", + "type": "shell", + "command": ". ./env.sh && make -C test/test_lsp test", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 219d7289c..14107f372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +# CHANGELOG + * 2.6.1 - 2024-05-23 -``` + +```text fix IEEE module name prefix expectation thanks to Siddharth Sharma @@ -10,7 +13,8 @@ ``` * 2.6.0 - 2023-11-03 -``` + +```text lots of improvements to the UML plugin thanks to Nick Hancock lots of improvements to build and test @@ -36,7 +40,8 @@ ``` * 2.5.3 - 2022-03-30 -``` + +```text added support for checking 'ancestor' and 'ancestor-or-self' XPATH axes added new option --exclude-features which is used to prune the data model by removing all nodes that are defined with the @@ -61,12 +66,14 @@ ``` * 2.5.2 - 2021-12-02 -``` + +```text #774 - fixed regression in 2.5.1 in ietf plugin ``` * 2.5.1 - 2021-12-02 -``` + +```text #770 - ietf plugin: updated IETF Trust Legal Provisions statement #767 - fixed access issue in test (thanks to Duncan Eastoe) @@ -80,7 +87,8 @@ ``` * 2.5.0 - 2021-06-21 -``` + +```text moved automated tests from travis ci to github actions added new plugin for verifying 3GPP YANG authoring rules thanks to Balázs Lengyel @@ -102,7 +110,7 @@ * 2.4.0 - 2020-11-09 -``` +```text #690 - stop uses expanding if import circular dependency exists #685 - report errors in sample-xml-skeleton #683 - fix sample-xml-skeleton unknown namespace crash @@ -118,20 +126,20 @@ * 2.3.2 - 2020-07-06 -``` +```text #646 - config false deviation fix #587 - revert fix for #587; use xml schema regexp engine again ``` * 2.3.1 - 2020-06-29 -``` +```text Update Version Number ``` * 2.3.0 - 2020-06-28 -``` +```text add structure support (RFC8791) pr:639 - output all missing hello modules then exit @@ -173,7 +181,7 @@ * 2.2.1 - 2020-03-06 -``` +```text pr:576 - added all transforms to the release (specifically 'edit') thanks to William Lupton @@ -181,7 +189,7 @@ * 2.2 - 2020-03-05 -``` +```text pr:557 - added new options for customizing error messages thanks to @gribok pr:556 - extended parsing of deviation in hello @@ -219,7 +227,7 @@ * 2.1.1 - 2020-01-03 -``` +```text #532 - warn if config true xpath refers to config false node #522 - find prefixes in xpath expressions in groupings #518 - broken xpath check @@ -229,7 +237,7 @@ * 2.1 - 2019-10-20 -``` +```text added a plugin to generate SID files (see draft-ietf-core-sid) thanks to @lemikev fixed canonical stmt order in 'identity' @@ -243,7 +251,7 @@ * 2.0.2 - 2019-08-21 -``` +```text pr:497 - fixed crash when parsing an xpath union with three or more terms thanks to Stuart Bayley @@ -257,7 +265,7 @@ * 2.0.1 - 2019-06-11 -``` +```text pr:492 - ensure the ietf-netconf namespace isn't added multiple times in json2xml #493 - fixed crash with --keep-comments where comments were present @@ -265,9 +273,9 @@ #491 - fixed incorrect prototype for XPath "concat" function ``` -* 2.0 - 2019-05-29 +* 2.0 - 2019-05-29 -``` +```text pyang now has a proper XPath 1.0 parser, which means that it will detect more XPath errors, and produce warnings for XPath expressions that for example refer to unknown nodes @@ -322,7 +330,7 @@ * 1.7.8 - 2019-01-21 -``` +```text for python coders: reverted method signature change for Repository.get_module_from_handle(). it now has the same signature as in 1.7.5. @@ -333,13 +341,13 @@ * 1.7.7 - 2019-01-17 -``` +```text fixed a bug in -f yang formatting ``` * 1.7.6 - 2019-01-17 -``` +```text fixed grammar; do not allow "must" in "choice" added --yang-line-length to try to format lines with max length various fixes to -f yang for consistency in the output @@ -372,7 +380,7 @@ * 1.7.5 - 2018-04-25 -``` +```text the tree output is now aligned with RFC 8340 remove trailing whitespace from double quoted strings -f yang formatting fixes @@ -402,7 +410,7 @@ * 1.7.4 - 2018-02-23 -``` +```text the tree output is now aligned with draft-ietf-netmod-yang-tree-diagrams-05 added --tree-no-expand-uses to not expang groupings in uses @@ -434,7 +442,7 @@ * 1.7.3 - 2017-06-27 -``` +```text #318 - handle multiple rc:yang-data statements. this bug caused validation of ietf-restconf, or any module that imported ietf-restconf, to fail. @@ -442,7 +450,7 @@ * 1.7.2 - 2017-06-14 -``` +```text added support for external plugins, using setuptools entry_points added a warning for unsafe escape sequences in double quoted strings. @@ -475,7 +483,7 @@ * 1.7.1 - 2016-11-02 -``` +```text added support for RFC 7952, metadata annotations added --tree-max-length option @@ -498,7 +506,7 @@ * 1.7 - 2016-06-16 -``` +```text added support for YANG 1.1 added command line flag --ignore-error, thanks to Nick Weeds added option --tree-print-groupings to the 'tree' plugin @@ -520,7 +528,7 @@ * 1.6 - 2015-10-06 -``` +```text removed the deprecated, incomplete and erroneous XSD plugin - use the DSDL plugin instead. added new plugin: 'lint' to check if a module follow @@ -568,7 +576,7 @@ * 1.5 - 2014-11-18 -``` +```text added new plugin: 'capability' to print the capability string for a module added new plugin: 'check-update' which can be used to compare @@ -601,13 +609,13 @@ * 1.4.1 - 2013-11-11 -``` +```text #96 - 1.4 doesn't work with Pyhton 3 ``` * 1.4 - 2013-10-24 -``` +```text added option --lax-xpath-checks deprecated the xsd output plugin tree: now prints augmented nodes @@ -628,7 +636,7 @@ * 1.3 - 2013-01-31 -``` +```text added new plugins: hypertree and jstree added new plugins: jsonxsl and jtox added command line flags -W and -E to treat warnings as errors @@ -664,7 +672,7 @@ * 1.2 - 2011-07-27 -``` +```text added edit-config target to yang2dsls if a submodule A includes submodule B, which includes @@ -682,7 +690,7 @@ * 1.1 - 2011-02-16 -``` +```text DSDL output compatible with RFC 6110 added uml output format @@ -703,7 +711,7 @@ * 1.0 - 2010-10-07 -``` +```text compatible with RFC 6020 added yang2dsdl(1) program @@ -715,7 +723,7 @@ * 0.9.3 - 2008-12-07 -``` +```text compatible with draft-ietf-netmod-yang-02 rewrote validation code. got rid of all the specialized classes handle circular defintions @@ -734,7 +742,7 @@ * 0.9.2 - 2008-10-13 -``` +```text handle prefixed references to local groupings make use of path argument given to pyang handle multiple patterns @@ -772,7 +780,7 @@ * 0.9.1 - 2008-07-08 -``` +```text rewrote yang parser added yin parser added dsdl output @@ -782,13 +790,13 @@ * 0.9.0b - 2008-05-19 -``` +```text first release of restructured code ``` * 02.2 - 2008-02-21 -``` +```text fixed some xsd output bugs fixed bug in refinmenet, where a valid refinmened would generate a duplicate node definition error @@ -800,19 +808,19 @@ * 02.1 - 2008-02-06 -``` +```text draft-bjorklund-netconf-yang-02 compliant. ``` - + * 01.3 - 2008-02-01 -``` +```text draft-bjorklund-netconf-yang-01 compliant. ``` * 00.2 - 2008-01-15 -``` +```text fixed grouping translation in XSD output generate YIN appinfo by default in XSD output added validation of identifiers @@ -825,6 +833,6 @@ * 00.1 - 2007-11-14 -``` +```text Initial version, draft-bjorklund-netconf-yang-00 compliant. ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56ae10742..731c2bac8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,4 @@ -Contributing -============ +# Contributing Pull requests are welcome! @@ -7,9 +6,7 @@ Pull requests are welcome! * If applicable, update the man page - -Code style ----------- +## Code style * Do not introduce trailing whitespace. @@ -20,4 +17,3 @@ Hint for emacs users: (setq whitespace-style (quote (face trailing tabs lines))) and use whitespace-mode. - diff --git a/README.md b/README.md index 2b5491643..3e53c9df6 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,115 @@ -# pyang # +# pyang -[![Release](https://img.shields.io/github/v/release/mbj4668/pyang)](https://github.com/mbj4668/pyang/releases) [![Build Status](https://github.com/mbj4668/pyang/actions/workflows/tests.yml/badge.svg)](https://github.com/mbj4668/pyang/actions) +[![Release](https://img.shields.io/github/v/release/mbj4668/pyang)](https://github.com/mbj4668/pyang/releases) +[![Build Status](https://github.com/mbj4668/pyang/actions/workflows/tests.yml/badge.svg)](https://github.com/mbj4668/pyang/actions) -## Overview ## +## Overview pyang is a YANG validator, transformator and code generator, written in python. It can be used to validate YANG modules for correctness, to transform YANG modules into other formats, and to write plugins to generate code from the modules. -YANG ([RFC 7950](http://tools.ietf.org/html/rfc7950)) is a data modeling language for NETCONF ([RFC 6241](http://tools.ietf.org/html/rfc6241)), developed by the IETF [NETMOD](http://www.ietf.org/html.charters/netmod-charter.html) WG. +YANG ([RFC 7950][rfc7950]) is a data modeling language for NETCONF +([RFC 6241][rfc6241]), developed by the IETF [NETMOD][netmod] WG. -## Documentation ## +## Documentation See [Documentation](https://github.com/mbj4668/pyang/wiki/Documentation). -## Installation ## +## Installation -- **1 PyPI** +1. **PyPI** -Pyang can be installed from [PyPI](https://pypi.python.org/pypi): + Pyang can be installed from [PyPI](https://pypi.python.org/pypi): -```sh -# pip install pyang -``` + ```sh + pip install pyang + ``` -- **2 Source** +2. **Source** -> It is reccomended to use a [Python virtual environment](https://docs.python.org/3/tutorial/venv.html) + > It is recommended to use a [Python virtual environment][venv] -```sh - git clone https://github.com/mbj4668/pyang.git - cd pyang - pip install -e . -``` + ```sh + git clone https://github.com/mbj4668/pyang.git + cd pyang + pip install -e . + ``` + To install in a different location, run: -To install in a different location, run: + ```sh + python setup.py install --prefix=/usr/local + ``` -```sh - python setup.py install --prefix=/usr/local -``` + If you do this, it is recommended to set the environment variable + **YANG_INSTALL** to the prefix directory. This ensures that pyang will + find standard YANG modules. In addition, make sure that **PYTHONPATH** is + set to something as follows: -If you do this, it is recommended to set the environment variable -**YANG_INSTALL** to the prefix directory. This ensures that pyang will -find standard YANG modules. In addition, make sure that **PYTHONPATH** is set -to something as follows: + ```sh + export PYTHONPATH=/usr/local/lib/python3.10/site-packages + ``` -```sh -export PYTHONPATH=/usr/local/lib/python3.10/site-packages -``` + or whatever version of python you are running. -or whatever version of python you are running. + Run locally without installing + ```sh + export PATH=`pwd`/bin:$PATH + export MANPATH=`pwd`/man:$MANPATH + export PYTHONPATH=`pwd`:$PYTHONPATH + export YANG_MODPATH=`pwd`/modules:$YANG_MODPATH + export PYANG_XSLT_DIR=`pwd`/xslt + export PYANG_RNG_LIBDIR=`pwd`/schema + ``` -Run locally without installing + or: -```sh -export PATH=`pwd`/bin:$PATH -export MANPATH=`pwd`/man:$MANPATH -export PYTHONPATH=`pwd`:$PYTHONPATH -export YANG_MODPATH=`pwd`/modules:$YANG_MODPATH -export PYANG_XSLT_DIR=`pwd`/xslt -export PYANG_RNG_LIBDIR=`pwd`/schema -``` - -or: - -```sh -source ./env.sh -``` + ```sh + source ./env.sh + ``` -## Compatibility ## +## Compatibility pyang is compatible with the following IETF RFCs: - * [RFC 6020: YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)](https://tools.ietf.org/html/rfc6020) - * [RFC 6087: Guidelines for Authors and Reviewers of YANG Data Model Documents](https://tools.ietf.org/html/rfc6087) - * [RFC 6110: Mapping YANG to Document Schema Definition Languages and Validating NETCONF Content](https://tools.ietf.org/html/rfc6110) - * [RFC 6643: Translation of Structure of Management Information Version 2 (SMIv2) MIB Modules to YANG Modules](https://tools.ietf.org/html/rfc6643) - * [RFC 7950: The YANG 1.1 Data Modeling Languages](https://tools.ietf.org/html/rfc7950) - * [RFC 7952: Defining and Using Metadata with YANGs](https://tools.ietf.org/html/rfc7952) - * [RFC 8040: RESTCONF Protocols](https://tools.ietf.org/html/rfc8040) - * [RFC 8407: Guidelines for Authors and Reviewers of Documents Containing YANG Data Models](https://tools.ietf.org/html/rfc8407) - * [RFC 8791: YANG Data Structure Extensions](https://tools.ietf.org/html/rfc8791) - -## Features ## - - * Validate YANG modules. - * Convert YANG modules to YIN, and YIN to YANG. - * Translate YANG data models to DSDL schemas, which can be used for - validating various XML instance documents. See - [InstanceValidation](https://github.com/mbj4668/pyang/wiki/InstanceValidation). - * Generate UML diagrams from YANG models. See - [UMLOutput](https://github.com/mbj4668/pyang/wiki/UMLOutput) for - an example. - * Generate compact tree representation of YANG models for quick - visualization. See - [TreeOutput](https://github.com/mbj4668/pyang/wiki/TreeOutput) for - an example. - * Generate a skeleton XML instance document from the data model. - * Schema-aware translation of instance documents encoded in XML to - JSON and vice-versa. See - [XmlJson](https://github.com/mbj4668/pyang/wiki/XmlJson). - * Plugin framework for simple development of other outputs, such as - code generation. - -## Usage ## +* [RFC 6020: YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF)][rfc6020] +* [RFC 6087: Guidelines for Authors and Reviewers of YANG Data Model Documents][rfc6087] +* [RFC 6110: Mapping YANG to Document Schema Definition Languages and Validating NETCONF Content][rfc6110] +* [RFC 6643: Translation of Structure of Management Information Version 2 (SMIv2) MIB Modules to YANG Modules][rfc6643] +* [RFC 7950: The YANG 1.1 Data Modeling Languages][rfc7950] +* [RFC 7952: Defining and Using Metadata with YANGs][rfc7952] +* [RFC 8040: RESTCONF Protocols][rfc8040] +* [RFC 8407: Guidelines for Authors and Reviewers of Documents Containing YANG Data Models][rfc8407] +* [RFC 8791: YANG Data Structure Extensions][rfc8791] + +## Features + +* Validate YANG modules. +* Convert YANG modules to YIN, and YIN to YANG. +* Translate YANG data models to DSDL schemas, which can be used for + validating various XML instance documents. See + [InstanceValidation](https://github.com/mbj4668/pyang/wiki/InstanceValidation). +* Generate UML diagrams from YANG models. See + [UMLOutput](https://github.com/mbj4668/pyang/wiki/UMLOutput) for + an example. +* Generate compact tree representation of YANG models for quick + visualization. See + [TreeOutput](https://github.com/mbj4668/pyang/wiki/TreeOutput) for + an example. +* Generate a skeleton XML instance document from the data model. +* Schema-aware translation of instance documents encoded in XML to + JSON and vice-versa. See + [XmlJson](https://github.com/mbj4668/pyang/wiki/XmlJson). +* Plugin framework for simple development of other outputs, such as + code generation. +* Serve Microsoft [Language Server Protocol][lsp] for in editor support + for pyang provided features, such as diagnostics and formatting. See + [pyang-lsp](./doc/pyang-lsp.md) for further details. + +## Usage ```sh pyang -h @@ -118,70 +121,86 @@ or man pyang ``` -## Code structure ## +## Code structure -* **bin/** +* **`bin/`** Executable scripts. -* **pyang/** +* **`pyang/`** Contains the pyang library code. -* **pyang/__init__.py** +* **`pyang/__init__.py`** Initialization code for the pyang library. -* **pyang/context.py** +* **`pyang/context.py`** Defines the Context class, which represents a parsing session -* **pyang/repository.py** +* **`pyang/repository.py`** Defines the Repository class, which is used to access modules. -* **pyang/syntax.py** +* **`pyang/syntax.py`** Generic syntax checking for YANG and YIN statements. Defines regular expressions for argument checking of core statements. -* **pyang/grammar.py** +* **`pyang/grammar.py`** Generic grammar for YANG and YIN. Defines chk_module_statements() which validates a parse tree according to the grammar. -* **pyang/statements.py** +* **`pyang/statements.py`** Defines the generic Statement class and all validation code. -* **pyang/yang_parser.py** +* **`pyang/yang_parser.py`** YANG tokenizer and parser. -* **pyang/yin_parser.py** +* **`pyang/yin_parser.py`** YIN parser. Uses the expat library for XML parsing. -* **pyang/types.py** +* **`pyang/types.py`** Contains code for checking built-in types. -* **pyang/plugin.py** +* **`pyang/plugin.py`** Plugin API. Defines the class PyangPlugin which all plugins inherits from. All output handlers are written as plugins. -* **pyang/plugins/** +* **`pyang/plugins/`** Directory where plugins can be installed. All plugins in this directory are automatically initialized when the library is initialized. -* **pyang/scripts/** +* **`pyang/scripts/`** Directory where the python cli scripts are located. Installed as entry point. -* **pyang/translators/** +* **`pyang/translators/`** Contains output plugins for YANG, YIN, and DSDL translation. -* **xslt** +* **`pyang/lsp/`** + Contains Microsoft [Language Server Protocol][lsp] based YANG language server + using pyang backend. Lint plugin and all plugins based on it are used for + diagnostics, and YANG format output plugin is used for formatting. + +* **`xslt/`** Contains XSLT style sheets for generating RELAX NG, Schematron and DSRL schemas and validating instance documents. Also included is the free implementation of ISO Schematron by Rick Jelliffe from - http://www.schematron.com/ (files iso_schematron_skeleton_for_xslt1.xsl, - iso_abstract_expand.xsl and iso_svrl_for_xslt1.xsl). + (files `iso_schematron_skeleton_for_xslt1.xsl`, + `iso_abstract_expand.xsl` and `iso_svrl_for_xslt1.xsl`). -* **schema** +* **`schema/`** Contains RELAX NG schemas and pattern libraries. - - +[rfc6020]: https://tools.ietf.org/html/rfc6020 +[rfc6087]: https://tools.ietf.org/html/rfc6087 +[rfc6110]: https://tools.ietf.org/html/rfc6110 +[rfc6241]: https://tools.ietf.org/html/rfc6241 +[rfc6643]: https://tools.ietf.org/html/rfc6643 +[rfc7950]: https://tools.ietf.org/html/rfc7950 +[rfc7952]: https://tools.ietf.org/html/rfc7952 +[rfc8040]: https://tools.ietf.org/html/rfc8040 +[rfc8407]: https://tools.ietf.org/html/rfc8407 +[rfc8791]: https://tools.ietf.org/html/rfc8791 +[netmod]: https://www.ietf.org/html.charters/netmod-charter.html +[venv]: https://docs.python.org/3/tutorial/venv.html +[lsp]: https://microsoft.github.io/language-server-protocol/ diff --git a/TODO b/TODO deleted file mode 100644 index 435d6b7ec..000000000 --- a/TODO +++ /dev/null @@ -1,12 +0,0 @@ - o add validation of instance-identifier defaults - - o move all plugin API functions to plugin.py - - o give a warning if the default case does not have any default leafs - -Optimizations: - - o lazy read imported modules. do not validate the module on import, - but do it when something is used. even better would be to lazy - validate. hmm, maybe separate the side-effect free validation - functions from the functions that have side-effect (like expand). diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..f9aad0c60 --- /dev/null +++ b/TODO.md @@ -0,0 +1,14 @@ +# TODO + +* add validation of instance-identifier defaults + +* move all plugin API functions to plugin.py + +* give a warning if the default case does not have any default leafs + +## Optimizations + +* lazy read imported modules. do not validate the module on import, + but do it when something is used. even better would be to lazy + validate. hmm, maybe separate the side-effect free validation + functions from the functions that have side-effect (like expand). diff --git a/dev-requirements.txt b/dev-requirements.txt index dae372d67..fe8a2b170 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,4 +3,5 @@ pycodestyle pyflakes flake8 pylint -virtualenv \ No newline at end of file +virtualenv +pygls diff --git a/doc/img/lsp-eglot.png b/doc/img/lsp-eglot.png new file mode 100644 index 000000000..45a2a6161 Binary files /dev/null and b/doc/img/lsp-eglot.png differ diff --git a/doc/img/lsp-vscode.png b/doc/img/lsp-vscode.png new file mode 100644 index 000000000..312ec3263 Binary files /dev/null and b/doc/img/lsp-vscode.png differ diff --git a/doc/json2xml.1.md b/doc/json2xml.1.md index 1f06b6bcc..ec674310b 100644 --- a/doc/json2xml.1.md +++ b/doc/json2xml.1.md @@ -16,7 +16,6 @@ model into XML. **json2xml** -h | -\-help - # DESCRIPTION This program translates *json_file* into XML using the procedure @@ -86,8 +85,9 @@ dhcp-data.xml. **RFC 7951**, **pyang**(1) - # AUTHOR **Ladislav Lhotka** <lhotka@nic.cz>\ CZ.NIC + + diff --git a/doc/pyang-lsp.md b/doc/pyang-lsp.md new file mode 100644 index 000000000..5ed5b9727 --- /dev/null +++ b/doc/pyang-lsp.md @@ -0,0 +1,325 @@ +# pyang LSP Server + +`pyang` can be executed as a [Microsoft LSP][lsp] Server for YANG Language. + +See **LSP Server** section in `pyang` [manpage](./pyang.1.md) for details on +related command line interface option arguments. + +[lsp]: https://microsoft.github.io/language-server-protocol/ + +## Server Capabilities + +pyang LSP server advertises the following capabilities as part of the response +in [Initialize Request][initialize] procedure as seen below. + +```json +{ + "id": 1, + "jsonrpc": "2.0", + "result": { + "capabilities": { + "positionEncoding": "utf-16", + "textDocumentSync": { + "openClose": true, + "change": 1, + "willSave": false, + "willSaveWaitUntil": false, + "save": false + }, + "documentFormattingProvider": true, + "executeCommandProvider": { + "commands": [] + }, + "diagnosticProvider": { + "interFileDependencies": true, + "workspaceDiagnostics": true, + "identifier": "pyang" + }, + "workspace": { + "workspaceFolders": { + "supported": true, + "changeNotifications": true + }, + "fileOperations": {} + } + }, + "serverInfo": { + "name": "pyang", + "version": "v0.1" + } + } +} +``` + +See following sections for detailed support statements including non-compliance. + +[initialize]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize + +## Configuration + +pyang does not support workspace configuration over LSP, yet. + +Any configurability is limited to the following channels, listed in ascending +order of precedence + +1. Defaults within pyang implementation +2. `pyang` command line arguments +3. LSP client provided params in context of an rpc/notification + +The pyang defaults, command line arguments, and LSP client rpc/notification +details per feature can be found in specific [Features](#features) sections. + +## Features + +### Diagnostics + +LSP provides following procedures to diagnose language content. + +* [Publish Diagnostics][textDocument_publishDiagnostics] +* [Pull Diagnostics][textDocument_pullDiagnostics] + +pyang supports both the above procedures. + +pyang skips statement keyword and grammar checks if there are YANG parser errors +which prevent further validation. Syntax errors which prevent parsing of the +whole file by pyang need to be fixed before further diagnostics are shared over +LSP to the client. + +[textDocument_publishDiagnostics]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics +[textDocument_pullDiagnostics]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics + +#### Configuration + +pyang default diagnostics configuration parameters are as listed below, with +CLI based day0 setting at server instantiation. + +Parameter | Default | CLI | LSP | +------------------------------|---------|-----------------------------|-----| +**RFC 8407** Checks Enabled | False | **--lint** | N/A | +Strict YANG Compliance | False | **--strict** | N/A | +Canonical Order | False | **--canonical** | N/A | +Disable All Warnings | False | **-Wnone** | N/A | +Treat an Error as a Warning | N/A | **-W** _errorcode_ | N/A | +Treat a Warning as an Error | N/A | **-E** _errorcode_ | N/A | +Treat All Warnings as Errors | False | **-Werror** | N/A | +Maximum Line Length | 80 | **--max-line-length** | N/A | +Maximum Identifier Length | 64 | **--max-identifier-length** | N/A | + +LSP based day1/day2 configuration override mechanism can be introduced later. + +#### Diagnostic Context + +pyang currently only stores the error context at the granularity of file and +line numbers. This resolution prevents high precision error context underlines +for symbols or arguments, that are dependent on a range of line and column tuple. + +LSP [`Diagnostic`][diagnostic] [`Range`][range] is currently set to the whole +line. + +[diagnostic]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic +[range]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range + +#### Diagnostic Severity + +pyang validation error levels are translated to LSP diagnostic severities as +mapped in the table below + +pyang `level` (`type`) | LSP [`diagnosticSeverity`][diagnosticSeverity] | +-----------------------|------------------------------------------------| +**1** (Critical Error) | Error | +**2** (Major Error) | Error | +**3** (Minor Error) | Warning | +**4** (Warning) | Information | + +[diagnosticSeverity]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity + +#### Other Diagnostic Metadata + +[`DiagnosticTag`][diagnosticTag] `Unnecessary` is reported for `UNUSED_*` error +codes. + +[`DiagnosticRelatedInformation`][diagnosticRelatedInformation] is reported for +`DUPLICATE_*` error codes. + +[diagnosticTag]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag +[diagnosticRelatedInformation]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticRelatedInformation + +#### Demo + +##### GNU Emacs Diagnostics + +> ![pyang diagnostics view in GNU Emacs+Eglot](./img/lsp-eglot.png "Title") +> _Diagnostics view using GNU Emacs via its [Eglot][eglot] LSP client submodule_ + +See [LSP Client Settings](#gnu-emacs-settings) for details. + +[eglot]: https://www.gnu.org/software/emacs/manual/html_mono/eglot.html + +##### Microsoft Visual Studio Code Diagnostics + +> ![pyang diagnostics view in Visual Studio Code](./img/lsp-vscode.png "Title") +> _Diagnostics view using [Visual Studio Code][vscode] via a language extension_ + +See [LSP Client Settings](#microsoft-visual-studio-code-settings) for details. + +[vscode]: https://www.gnu.org/software/emacs/manual/html_mono/eglot.html + +### Formatting + +LSP provides following procedures to format language content. + +* [Document Formatting Request][textDocument_formatting] as advertised via the + `documentFormattingProvider` capability +* [Document Range Formatting Request][textDocument_rangeFormatting] as + advertised via the `documentRangeFormattingProvider` capability +* [Document on Type Formatting Request][textDocument_onTypeFormatting] as + advertised via the `documentOnTypeFormattingProvider` capability + +Out of the above, pyang only supports `documentFormattingProvider`, since the +mechanism used to format is based on YANG to YANG "translation". This also has +an implication that formatting request is only handled for a consistent YANG +file, i.e., no sytactical errors exist, e.g., `EOF_ERROR` or `SYNTAX_ERROR`. + +#### Configuration + +pyang default formatting configuration parameters are as listed below, with +CLI based day0 and LSP based day1/day2 configuration setting/override mechanisms. + +Parameter | Default | CLI | LSP (For ~~unsupported~~, see below) | +--------------------------|---------|-----------------------------------|---------------------------------------------------| +Indentation Size | 2 | **--yang-indent-size** | [`tabSize`][formattingOptions] | +Indentation Style | Spaces | N/A | [~~`insertSpaces`~~][formattingOptions] | +Trim Trailing Whitespace | True | N/A | [~~`trimTrailingWhitespace`~~][formattingOptions] | +Insert Final Newline | True | N/A | [`insertFinalNewline`][formattingOptions] | +Trim Final Newlines | True | N/A | [~~`trimFinalNewlines`~~][formattingOptions] | +Maximum Line Length | 80 | **--yang-line-length** | N/A | +Canonical Order | False | **--yang-canonical** | N/A | +Remove Unused Imports | False | **--yang-remove-unused-imports** | N/A | +Remove Comments | False | **--yang-remove-comments** | N/A | + +Unsupported [`FormattingOptions`][formattingOptions] are handled as per default +settings as described below + +* `insertSpaces` + > Tab characters are always replaced with spaces as per `tabSize` +* `trimTrailingWhitespace` + > Trailing whitespace characters are always trimmed +* `trimFinalNewlines` + > Final newlines are always trimmed + +[textDocument_formatting]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting +[textDocument_rangeFormatting]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rangeFormatting +[textDocument_onTypeFormatting]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_onTypeFormatting +[formattingOptions]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#formattingOptions + +## Annex A Editor/IDE and LSP Client Settings + +### General Editor/IDE Settings + +To avoid per editor configuration of formatting parameters per project, +it is recommended to use editorconfig. it also allows for differential +formatting settings per directory or file in a project. + +Example editorconfig file at root of the project as below. + +```ini +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yang] +indent_style = space +indent_size = 2 +``` + +### GNU Emacs Settings + +General plugins and settings which are required or enhance the experience are +as in configuration below + +```lisp +;; https://github.com/mbj4668/yang-mode/blob/master/yang-mode.el +(require 'yang-mode) + +(defun my-yang-mode-hook () + "Configuration for YANG Mode. Add this to `yang-mode-hook'." + (if window-system + (progn + (c-set-style "BSD") + (setq indent-tabs-mode nil) + (setq c-basic-offset 2) + (setq font-lock-maximum-decoration t) + (font-lock-mode t)))) +(add-hook 'yang-mode-hook 'my-yang-mode-hook) + +;; https://www.gnu.org/software/emacs/manual/html_node/emacs/Outline-Mode.html +(defun show-onelevel () + "show entry and children in outline mode" + (interactive) + (outline-show-entry) + (outline-show-children)) +(defun my-outline-bindings () + "sets shortcut bindings for outline minor mode" + (interactive) + (local-set-key [?\C-,] 'hide-body) + (local-set-key [?\C-.] 'show-all) + (local-set-key [C-up] 'outline-previous-visible-heading) + (local-set-key [C-down] 'outline-next-visible-heading) + (local-set-key [C-left] 'hide-subtree) + (local-set-key [C-right] 'show-onelevel) + (local-set-key [M-up] 'outline-backward-same-level) + (local-set-key [M-down] 'outline-forward-same-level) + (local-set-key [M-left] 'outline-hide-subtree) + (local-set-key [M-right] 'outline-show-subtree)) +(add-hook + 'outline-minor-mode-hook + 'my-outline-bindings) +(defconst sort-of-yang-identifier-regexp "[-a-zA-Z0-9_\\.:]*") +(add-hook + 'yang-mode-hook + '(lambda () + (outline-minor-mode) + (setq outline-regexp + (concat "^ *" sort-of-yang-identifier-regexp " *" + sort-of-yang-identifier-regexp + " *{")))) + +;; https://github.com/editorconfig/editorconfig-emacs/blob/master/README.md +(editorconfig-mode 1) +``` + +#### Eglot (built-in since Emacs v29) + +Works with built-in Emacs modules. + +```lisp +;; https://www.gnu.org/software/emacs/manual/html_mono/eglot.html +(require 'eglot) +(add-hook 'yang-mode-hook 'eglot-ensure) +(with-eval-after-load 'eglot + (add-to-list 'eglot-server-programs + '(yang-mode . ("pyang" + "--lsp" + "--lint" + "--canonical" + "--max-line-length=80" + "--max-identifier-length=32")))) +``` + +### Microsoft Visual Studio Code Settings + +General extensions which assist experience are as below + +* [editorconfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) + +#### pyang (WIP extension) + +An extension (currently private) is built from scratch to validate the LSP +server implementation. diff --git a/doc/pyang.1.md b/doc/pyang.1.md index e0fa484b9..3916eab44 100644 --- a/doc/pyang.1.md +++ b/doc/pyang.1.md @@ -6,6 +6,7 @@ footer: pyang-_VERSION_ date: _DATE_ --- # NAME + pyang - validate and convert YANG modules to various formats # SYNOPSIS @@ -30,7 +31,6 @@ pyang - validate and convert YANG modules to various formats **pyang** -v -\-version - One or more *file* parameters may be given on the command line. They denote either YANG modules to be processed (in YANG or YIN syntax) or, using the **-\-hello** switch, a server <hello> message @@ -152,6 +152,10 @@ program exits with exit code 0 if all modules are valid. and that the module has the correct license text and **RFC 2119** / **RFC 8174** boilerplate text. +**-\-ieee** +: Validate the module(s) like **-\-lint**, and in addition verifies + that the namespace and module name follow the IEEE conventions. + **-\-lax-quote-checks** : Lax checks of backslashes in double quoted strings in YANG version 1 modules. **RFC 6020** does not clearly define how to handle @@ -208,6 +212,10 @@ program exits with exit code 0 if all modules are valid. **-o** **-\-output** _outfile_ : Write the output to the file _outfile_ instead of stdout. +**-l** **-\-lsp** _lsp_ +: Run as Microsoft LSP server instead of CLI tool. The supported LSP + modes and configuration options are listed in LSP SERVER below. + **-F** **-\-features** _features_ : _features_ is a string of the form _modulename_:[_feature_(,_feature_)*] @@ -530,6 +538,43 @@ Options for the *lint* checker: : Validate that all identifiers use hyphenated style, i.e., no uppercase letters or underscores. +# LSP SERVER + +The *lsp* option switches **pyang** to run as a Microsoft LSP Server. +See https://microsoft.github.io/language-server-protocol/. + +When running as an LSP server, no files are required to be provided as +arguments and if provided they are ignored. + +*lint* backend is used to serve the *diagnosticProvider* capability with +respect for all *lint* checker options and plugin extension capability. + +_format_ backend is used to serve the *documentFormattingProvider* +capability via the *yang* output plugin. + +Options for *lsp*: + +**-\-lsp-mode** *mode* +: The mode options for the *lsp* service are listed in **pyang -h**: + + *tcp* + : TCP mode, generally used for debugging. See **-\-lsp-host** and + **-\-lsp-port**. + + *io* + : STDIO mode, generally used for production. + + *ws* + : WEBSOCKET mode, generally used to serve browser based editors. + See **-\-lsp-host** and **-\-lsp-port**. + +**-\-lsp-host** *host* +: The host name or IP address LSP service is running on. Default is + *127.0.0.1* + +**-\-lsp-port** *port* +: The TCP port LSP service is running on. Default is *2087*. + # YANG SCHEMA ITEM IDENTIFIERS (SID) YANG Schema Item iDentifiers (SID) are globally unique unsigned @@ -623,7 +668,7 @@ Options for generating, updating and checking .sid files: $ pyang --sid-update-file toaster@2009-11-20.sid \ toaster@2009-12-28.yang --sid-extra-range count -# CAPABILITY OUTPUT> +# CAPABILITY OUTPUT The *capability* output prints a capability URL for each module of the input data model, taking into account features and deviations, as @@ -1015,6 +1060,9 @@ Options for the *yang* output format: **-\-yang-remove-comments** : Remove all comments from the output. +**-\-yang-indent-size** *size* +: Format each line with a maximum indentation of *size*. Default is 2. + **-\-yang-line-length** *len* : Try to format each line with a maximum line length of *len*. Does not reformat long lines within strings. @@ -1125,3 +1173,5 @@ only for basic syntax errors. # AUTHORS See the file CONTRIBUTORS at https://github.com/mbj4668/pyang. + + diff --git a/doc/yang2dsdl.1.md b/doc/yang2dsdl.1.md index 997c231f4..bafd7c8c2 100644 --- a/doc/yang2dsdl.1.md +++ b/doc/yang2dsdl.1.md @@ -47,7 +47,6 @@ directory must be writable. Metadata annotations are also supported, if they are defined and used as described in **RFC 7952**. - The script can be executed by any shell interpreter compatible with POSIX.2, such as **bash**(1) or **dash**(1). @@ -88,7 +87,6 @@ rpc-reply notification : An event notification defined in an input YANG module. - The output schemas are contained in the following four files whose names depend on the arguments *basename* and *target*: @@ -255,7 +253,6 @@ rpc-rock.yang. 3 : Instance validation failed - # BUGS 1. The logic of command-line arguments may not be able to distinguish @@ -266,8 +263,9 @@ rpc-rock.yang. **pyang**(1), **xsltproc**(1), **xmllint**(1), **RFC 61110**. - # AUTHOR **Ladislav Lhotka** <lhotka@nic.cz>\ CZ.NIC + + diff --git a/etc/bash_completion.d/pyang b/etc/bash_completion.d/pyang index 739aeaa2a..2db9c8839 100644 --- a/etc/bash_completion.d/pyang +++ b/etc/bash_completion.d/pyang @@ -8,6 +8,8 @@ _pyang() local formats="hypertree dsdl depend sample-xml-skeleton omni yin tree jstree capability yang xsd uml jtox jsonxsl xmi name" + local lspmodes="io tcp ws" + local opts_global=" -h --help -v --version @@ -22,6 +24,7 @@ _pyang() --max-identifier-length -f --format -o --output + -l --lsp -F --features --deviation-module -p --path @@ -34,8 +37,8 @@ _pyang() --check-update-from -P --check-update-from-path --ietf - --lint - --lint-ensure-hyphenated-names" + --ieee + --lint" local opts_capability="--capability-entity" @@ -97,12 +100,25 @@ _pyang() local opts_yang=" --yang-canonical - --yang-remove-unused-imports" + --yang-remove-unused-imports + --yang-remove-comments + --yang-max-line-length + --yang-indent-size" local opts_yin=" --yin-canonical --yin-pretty-strings" + local opts_lint=" + --lint-namespace-prefix + --lint-modulename-prefix + --lint-ensure-hyphenated-names" + + local opts_lsp=" + --lsp-mode + --lsp-host + --lsp-port" + COMPREPLY=() _get_comp_words_by_ref cur prev cword words @@ -114,6 +130,12 @@ _pyang() -L|--hello) hello=yes ;; + --lint|--ietf|--ieee|--threegpp|--mef) + plugin=lint + ;; + -l|--lsp) + plugin=lsp + ;; esac ((wind++)) done @@ -173,6 +195,10 @@ _pyang() COMPREPLY=($(compgen -W '$formats' -- "$cur")) return 0 ;; + --lsp-mode) + COMPREPLY=($(compgen -W '$lspmodes' -- "$cur")) + return 0 + ;; esac if [[ $cur == -* ]]; then diff --git a/man/man1/pyang.1 b/man/man1/pyang.1 index cb2313f62..21e88f605 100644 --- a/man/man1/pyang.1 +++ b/man/man1/pyang.1 @@ -193,6 +193,10 @@ that the namespace and module name follow the IETF conventions, and that the module has the correct license text and \f[B]RFC 2119\f[R] / \f[B]RFC 8174\f[R] boilerplate text. .TP +\f[B]--ieee\f[R] +Validate the module(s) like \f[B]--lint\f[R], and in addition verifies +that the namespace and module name follow the IEEE conventions. +.TP \f[B]--lax-quote-checks\f[R] Lax checks of backslashes in double quoted strings in YANG version 1 modules. @@ -254,6 +258,11 @@ The supported formats are listed in OUTPUT FORMATS below. \f[B]-o\f[R] \f[B]--output\f[R] \f[I]outfile\f[R] Write the output to the file \f[I]outfile\f[R] instead of stdout. .TP +\f[B]-l\f[R] \f[B]--lsp\f[R] \f[I]lsp\f[R] +Run as Microsoft LSP server instead of CLI tool. +The supported LSP modes and configuration options are listed in LSP +SERVER below. +.TP \f[B]-F\f[R] \f[B]--features\f[R] \f[I]features\f[R] \f[I]features\f[R] is a string of the form \f[I]modulename\f[R]:[\f[I]feature\f[R](,\f[I]feature\f[R])*] @@ -656,6 +665,49 @@ Validate that the module\[cq]s name starts with \f[I]prefix\f[R]. \f[B]--lint-ensure-hyphenated-names\f[R] Validate that all identifiers use hyphenated style, i.e., no uppercase letters or underscores. +.SH LSP SERVER +.PP +The \f[I]lsp\f[R] option switches \f[B]pyang\f[R] to run as a Microsoft +LSP Server. +See https://microsoft.github.io/language-server-protocol/. +.PP +When running as an LSP server, no files are required to be provided as +arguments and if provided they are ignored. +.PP +\f[I]lint\f[R] backend is used to serve the \f[I]diagnosticProvider\f[R] +capability with respect for all \f[I]lint\f[R] checker options and +plugin extension capability. +.PP +\f[I]format\f[R] backend is used to serve the +\f[I]documentFormattingProvider\f[R] capability via the \f[I]yang\f[R] +output plugin. +.PP +Options for \f[I]lsp\f[R]: +.TP +\f[B]--lsp-mode\f[R] \f[I]mode\f[R] +The mode options for the \f[I]lsp\f[R] service are listed in \f[B]pyang +-h\f[R]: +.RS +.TP +\f[I]tcp\f[R] +TCP mode, generally used for debugging. +See \f[B]--lsp-host\f[R] and \f[B]--lsp-port\f[R]. +.TP +\f[I]io\f[R] +STDIO mode, generally used for production. +.TP +\f[I]ws\f[R] +WEBSOCKET mode, generally used to serve browser based editors. +See \f[B]--lsp-host\f[R] and \f[B]--lsp-port\f[R]. +.RE +.TP +\f[B]--lsp-host\f[R] \f[I]host\f[R] +The host name or IP address LSP service is running on. +Default is \f[I]127.0.0.1\f[R] +.TP +\f[B]--lsp-port\f[R] \f[I]port\f[R] +The TCP port LSP service is running on. +Default is \f[I]2087\f[R]. .SH YANG SCHEMA ITEM IDENTIFIERS (SID) .PP YANG Schema Item iDentifiers (SID) are globally unique unsigned integers @@ -791,7 +843,7 @@ $ pyang --sid-update-file toaster\[at]2009-11-20.sid \[rs] \f[R] .fi .RE -.SH CAPABILITY OUTPUT> +.SH CAPABILITY OUTPUT .PP The \f[I]capability\f[R] output prints a capability URL for each module of the input data model, taking into account features and deviations, as diff --git a/pyang/context.py b/pyang/context.py index 84938ac9f..b034ad935 100644 --- a/pyang/context.py +++ b/pyang/context.py @@ -1,6 +1,7 @@ """A parse session context""" import re +from typing import List, Tuple from . import error from . import yang_parser @@ -8,18 +9,19 @@ from . import util from . import statements from . import syntax +from . import repository class Context(object): """Class which encapsulates a parse session""" - def __init__(self, repository): + def __init__(self, repository: repository.Repository): """`repository` is a `Repository` instance""" - self.modules = {} + self.modules : dict[Tuple[str, str], statements.ModSubmodStatement] = {} """dict of (modulename,revision): contains all modules and submodule found""" - self.revs = {} + self.revs : dict[str, List[Tuple[str, Unknown]]] = {} """dict of modulename:[(revision,handle)] contains all modulenames and revisions found in the repository""" @@ -50,8 +52,7 @@ def internal_reset(self): self.modules = {} self.revs = {} self.errors = [] - for mod, rev, handle in self.repository.get_modules_and_revisions( - self): + for mod, rev, handle in self.repository.get_modules_and_revisions(self): if mod not in self.revs: self.revs[mod] = [] revs = self.revs[mod] @@ -78,7 +79,7 @@ def add_module(self, ref, text, in_format=None, else: p = yang_parser.YangParser() - module = p.parse(self, ref, text) + module : statements.ModSubmodStatement | None = p.parse(self, ref, text) if module is None: return None @@ -118,7 +119,7 @@ def add_module(self, ref, text, in_format=None, return self.add_parsed_module(module) - def add_parsed_module(self, module): + def add_parsed_module(self, module: statements.ModSubmodStatement): if module is None: return None if module.arg is None: @@ -141,17 +142,24 @@ def add_parsed_module(self, module): return module - def del_module(self, module): + def del_module(self, module, revision=None): """Remove a module from the context""" - rev = util.get_latest_revision(module) - del self.modules[(module.arg, rev)] + if revision is None: + revision = util.get_latest_revision(module) + revs = self.revs[module.arg] + for (rev, handle) in revs: + if rev == revision: + revs.remove((rev, handle)) + if not revs: + del self.revs[module.arg] + del self.modules[(module.arg, revision)] def get_module(self, modulename, revision=None): """Return the module if it exists in the context""" if revision is None and modulename in self.revs: (revision, _handle) = self._get_latest_rev(self.revs[modulename]) if revision is not None: - if (modulename,revision) in self.modules: + if (modulename, revision) in self.modules: return self.modules[(modulename, revision)] else: return None @@ -209,7 +217,7 @@ def search_module(self, pos, modulename, revision=None, # this module doesn't exist in the repos at all error.err_add(self.errors, pos, 'MODULE_NOT_FOUND', modulename) # keep track of this to avoid multiple errors - self.revs[modulename] = [] + # self.revs[modulename] = [] return None elif not self.revs[modulename]: # this module doesn't exist in the repos at all, error reported diff --git a/pyang/lsp/__init__.py b/pyang/lsp/__init__.py new file mode 100644 index 000000000..ae4afec60 --- /dev/null +++ b/pyang/lsp/__init__.py @@ -0,0 +1 @@ +"""pyang library to serve Microsoft LSP""" diff --git a/pyang/lsp/server.py b/pyang/lsp/server.py new file mode 100644 index 000000000..c90580072 --- /dev/null +++ b/pyang/lsp/server.py @@ -0,0 +1,548 @@ +"""pyang LSP Server""" + +from __future__ import absolute_import +import optparse +import os +import tempfile +from typing import List + +from pyang import error +from pyang import yang_parser +from pyang import context +from pyang import plugin +from pyang import syntax +from pyang.statements import Statement, ModSubmodStatement +from pyang.translators import yang + +from lsprotocol import types as lsp +from pygls.server import LanguageServer +from pygls.workspace import TextDocument +from pygls.uris import from_fs_path, to_fs_path + +import importlib + +ext_deps = ['pygls'] +def try_import_deps(): + """Raises `ModuleNotFoundError` if external module dependencies are missing""" + for dep in ext_deps: + importlib.import_module(dep) + +SERVER_NAME = "pyang" +SERVER_VERSION = "v0.1" + +SERVER_MODE_IO = "io" +SERVER_MODE_TCP = "tcp" +SERVER_MODE_WS = "ws" +supported_modes = [ + SERVER_MODE_IO, + SERVER_MODE_TCP, + SERVER_MODE_WS, +] +default_mode = SERVER_MODE_IO +default_host = "127.0.0.1" +default_port = 2087 + +# Diagnostics/Formatting parameters +default_line_length = 80 +default_canonical_order = False +default_remove_unused_imports = False +default_remove_comments = False + +class PyangLanguageServer(LanguageServer): + def __init__(self): + self.ctx : context.Context + self.yangfmt : yang.YANGPlugin + self.modules : dict[str, ModSubmodStatement] = {} + super().__init__( + name=SERVER_NAME, + version=SERVER_VERSION, + text_document_sync_kind=lsp.TextDocumentSyncKind.Full + ) + +pyangls = PyangLanguageServer() + + +def add_opts(optparser: optparse.OptionParser): + optlist = [ + # use capitalized versions of std options help and version + optparse.make_option("--lsp-mode", + dest="pyangls_mode", + default=default_mode, + metavar="LSP_MODE", + help="Provide LSP Service in this mode" \ + "Supported LSP server modes are: " + + ', '.join(supported_modes)), + optparse.make_option("--lsp-host", + dest="pyangls_host", + default=default_host, + metavar="LSP_HOST", + help="Bind LSP Server to this address"), + optparse.make_option("--lsp-port", + dest="pyangls_port", + type="int", + default=default_port, + metavar="LSP_PORT", + help="Bind LSP Server to this port"), + ] + g = optparser.add_option_group("LSP Server specific options") + g.add_options(optlist) + + +def start_server(optargs, ctx: context.Context, fmts: dict): + pyangls.ctx = ctx + pyangls.yangfmt = fmts['yang'] + if optargs.pyangls_mode == SERVER_MODE_TCP: + pyangls.start_tcp(optargs.pyangls_host, optargs.pyangls_port) + elif optargs.pyangls_mode == SERVER_MODE_WS: + pyangls.start_ws(optargs.pyangls_host, optargs.pyangls_port) + else: + pyangls.start_io() + +def _delete_from_ctx(text_doc: TextDocument): + if not pyangls.modules: + return + try: + module = pyangls.modules[text_doc.uri] + except KeyError: + return + pyangls.ctx.del_module(module) + del pyangls.modules[text_doc.uri] + +def _add_to_ctx(text_doc: TextDocument): + assert text_doc.filename + m = syntax.re_filename.search(text_doc.filename) + if m is not None: + name, rev, in_format = m.groups() + assert in_format == 'yang' + module = pyangls.ctx.add_module(text_doc.path, text_doc.source, + in_format, name, rev, + expect_failure_error=False, + primary_module=True) + else: + module = pyangls.ctx.add_module(text_doc.path, text_doc.source, + primary_module=True) + if module: + pyangls.modules[text_doc.uri] = module + return module + +def _update_ctx_module(text_doc: TextDocument): + _delete_from_ctx(text_doc) + return _add_to_ctx(text_doc) + +def _update_ctx_modules(): + for text_doc in pyangls.workspace.documents.values(): + _delete_from_ctx(text_doc) + for text_doc in pyangls.workspace.documents.values(): + _add_to_ctx(text_doc) + +def _get_ctx_modules(): + modules = [] + for k in pyangls.ctx.modules: + m = pyangls.ctx.modules[k] + if m is not None: + modules.append(m) + return modules + +def _clear_stmt_validation(stmt: Statement): + stmt.i_is_validated = False + substmt : Statement + for substmt in stmt.substmts: + _clear_stmt_validation(substmt) + +def _clear_ctx_validation(): + # pyangls.ctx.internal_reset() + pyangls.ctx.errors = [] + module : Statement + for module in pyangls.ctx.modules.values(): + module.internal_reset() + # _clear_stmt_validation(module) + +def _validate_ctx_modules(): + # ls.show_message_log("Validating YANG...") + modules = _get_ctx_modules() + + p : plugin.PyangPlugin + + for p in plugin.plugins: + p.pre_validate_ctx(pyangls.ctx, modules) + + pyangls.ctx.validate() + + for _m in modules: + _m.prune() + + for p in plugin.plugins: + p.post_validate_ctx(pyangls.ctx, modules) + +def _build_doc_diagnostics(ref: str) -> List[lsp.Diagnostic]: + """Builds lsp diagnostics from pyang context""" + diagnostics = [] + pyangls.ctx.errors.sort(key=lambda e: (e[0].ref, e[0].line), reverse=True) + for epos, etag, eargs in pyangls.ctx.errors: + if epos.ref != ref: + continue + msg = error.err_to_str(etag, eargs) + + def line_to_lsp_range(line) -> lsp.Range: + # pyang just stores line context, not keyword/argument context + start_line = line - 1 + if etag == 'LONG_LINE' and pyangls.ctx.max_line_len is not None: + start_col = pyangls.ctx.max_line_len + else: + start_col = 0 + end_line = line + end_col = 0 + return lsp.Range( + start=lsp.Position(line=start_line, character=start_col), + end=lsp.Position(line=end_line, character=end_col), + ) + + def level_to_lsp_severity(level) -> lsp.DiagnosticSeverity: + if level == 1 or level == 2: + return lsp.DiagnosticSeverity.Error + elif level == 3: + return lsp.DiagnosticSeverity.Warning + elif level == 4: + return lsp.DiagnosticSeverity.Information + else: + return lsp.DiagnosticSeverity.Hint + + diag_tags=[] + rel_info=[] + unused_etags = [ + 'UNUSED_IMPORT', + 'UNUSED_TYPEDEF', + 'UNUSED_GROUPING', + ] + duplicate_1_etags = [ + 'DUPLICATE_ENUM_NAME', + 'DUPLICATE_ENUM_VALUE', + 'DUPLICATE_BIT_POSITION', + 'DUPLICATE_CHILD_NAME', + ] + if etag in unused_etags: + diag_tags.append(lsp.DiagnosticTag.Unnecessary) + elif etag in duplicate_1_etags: + if etag == 'DUPLICATE_ENUM_NAME': + dup_arg = 1 + dup_msg = 'Original Enumeration' + elif etag == 'DUPLICATE_ENUM_VALUE': + dup_arg = 1 + dup_msg = 'Original Enumeration with Value' + elif etag == 'DUPLICATE_BIT_POSITION': + dup_arg = 1 + dup_msg = 'Original Bit Position' + elif etag == 'DUPLICATE_CHILD_NAME': + dup_arg = 3 + dup_msg = 'Original Child' + dup_uri = from_fs_path(eargs[dup_arg].ref) + dup_range = line_to_lsp_range(eargs[dup_arg].line) + if dup_uri: + dup_loc = lsp.Location(uri=dup_uri, range=dup_range) + rel_info.append(lsp.DiagnosticRelatedInformation(location=dup_loc, + message=dup_msg)) + elif etag == 'DUPLICATE_NAMESPACE': + # TODO + pass + + d = lsp.Diagnostic( + range=line_to_lsp_range(epos.line), + message=msg, + severity=level_to_lsp_severity(error.err_level(etag)), + tags=diag_tags, + related_information=rel_info, + code=etag, + source=SERVER_NAME, + ) + + diagnostics.append(d) + + return diagnostics + +def _publish_doc_diagnostics(text_doc: TextDocument, + diagnostics: List[lsp.Diagnostic] | None = None): + if not pyangls.client_capabilities.text_document: + return + if not pyangls.client_capabilities.text_document.publish_diagnostics: + return + if not diagnostics: + diagnostics = _build_doc_diagnostics(text_doc.path) + pyangls.publish_diagnostics(text_doc.uri, diagnostics) + +def _publish_workspace_diagnostics(): + for text_doc in pyangls.workspace.text_documents.values(): + _publish_doc_diagnostics(text_doc) + +def _get_folder_yang_uris(folder_uri) -> List[str]: + """Recursively find all .yang files in the given folder.""" + folder = to_fs_path(folder_uri) + assert folder + yang_files = [] + for root, _, files in os.walk(folder): + file : str + for file in files: + if file.endswith(".yang") and not file.startswith('.#'): + yang_files.append(from_fs_path(os.path.join(root, file))) + return yang_files + +def _have_parser_errors() -> bool: + for _, etag, _ in pyangls.ctx.errors: + if etag in yang_parser.errors: + return True + return False + +def _format_yang(source: str, opts, module) -> str: + if opts.insert_spaces == False: + pyangls.log_trace("insert_spaces is currently restricted to True") + if opts.tab_size: + pyangls.ctx.opts.yang_indent_size = opts.tab_size # type: ignore + if opts.trim_trailing_whitespace == False: + pyangls.log_trace("trim_trailing_whitespace is currently restricted to True") + if opts.trim_final_newlines == False: + pyangls.log_trace("trim_final_newlines is currently restricted to True") + pyangls.ctx.opts.yang_canonical = default_canonical_order # type: ignore + pyangls.ctx.opts.yang_line_length = default_line_length # type: ignore + pyangls.ctx.opts.yang_remove_unused_imports = default_remove_unused_imports # type: ignore + pyangls.ctx.opts.yang_remove_comments = default_remove_comments # type: ignore + + pyangls.yangfmt.setup_fmt(pyangls.ctx) + tmpfd = tempfile.TemporaryFile(mode="w+", encoding="utf-8") + + pyangls.yangfmt.emit(pyangls.ctx, [module], tmpfd) + + tmpfd.seek(0) + fmt_text = tmpfd.read() + tmpfd.close() + + # pyang only supports unix file endings and inserts a final one if missing + if not opts.insert_final_newline and not source.endswith('\n'): + fmt_text.rstrip('\n') + + return fmt_text + + +@pyangls.feature(lsp.INITIALIZED) +def initialized( + ls: LanguageServer, + params: lsp.InitializedParams, +): + # ls.show_message("Received Initialized") + # TODO: Try sending first diagnostics notifications here + # Does not seem to work, hence sending on first workspace/didChangeConfiguration + pass + + +@pyangls.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION) +def did_change_configuration( + ls: LanguageServer, + params: lsp.DidChangeConfigurationParams +): + # ls.show_message("Received Workspace Did Change Configuration") + # TODO: Handle configuration changes including ignoring additional files/subdirs + + _clear_ctx_validation() + + if ls.workspace.folders: + # TODO: Handle more than one workspace folder + folder = next(iter(ls.workspace.folders.values())) + yang_uris = _get_folder_yang_uris(folder.uri) + for yang_uri in yang_uris: + if not yang_uri in ls.workspace.text_documents.keys(): + yang_file = to_fs_path(yang_uri) + assert yang_file + with open(yang_file, 'r') as file: + yang_source = file.read() + file.close() + ls.workspace.put_text_document( + lsp.TextDocumentItem( + uri=yang_uri, + language_id='yang', + version=0, + text=yang_source, + ) + ) + + _update_ctx_modules() + + _validate_ctx_modules() + _publish_workspace_diagnostics() + + +@pyangls.feature(lsp.WORKSPACE_DID_CHANGE_WATCHED_FILES) +def did_change_watched_files( + ls: LanguageServer, + params: lsp.DidChangeWatchedFilesParams +): + """Workspace did change watched files notification.""" + # ls.show_message("Received Workspace Did Change Watched Files") + _clear_ctx_validation() + + # Process all the Deleted events first to handle renames more gracefully + for event in params.changes: + if event.type != lsp.FileChangeType.Deleted: + continue + + text_doc = ls.workspace.get_text_document(event.uri) + ls.workspace.remove_text_document(text_doc.uri) + _delete_from_ctx(text_doc) + _publish_doc_diagnostics(text_doc, []) + + for event in params.changes: + if event.type == lsp.FileChangeType.Created: + yang_file = to_fs_path(event.uri) + assert yang_file + with open(yang_file, 'r') as file: + yang_source = file.read() + file.close() + ls.workspace.put_text_document( + lsp.TextDocumentItem( + uri=event.uri, + language_id='yang', + version=0, + text=yang_source, + ) + ) + elif event.type == lsp.FileChangeType.Changed: + text_doc = ls.workspace.get_text_document(event.uri) + text_doc._source = None + + _update_ctx_modules() + _validate_ctx_modules() + _publish_workspace_diagnostics() + + +@pyangls.feature( + lsp.TEXT_DOCUMENT_DIAGNOSTIC, + lsp.DiagnosticOptions( + identifier=SERVER_NAME, + inter_file_dependencies=True, + workspace_diagnostics=True, + ), +) +def text_document_diagnostic( + params: lsp.DocumentDiagnosticParams, +) -> lsp.DocumentDiagnosticReport: + """Returns diagnostic report.""" + # pyangls.show_message("Received Text Document Diagnostic") + if pyangls.client_capabilities.text_document is None or \ + pyangls.client_capabilities.text_document.diagnostic is None: + pyangls.show_message("Unexpected textDocument/diagnostic from incapable client.") + text_doc = pyangls.workspace.get_text_document(params.text_document.uri) + doc_items = _build_doc_diagnostics(text_doc.path) + if doc_items is None: + items = [] + else: + items = doc_items + # TODO: check if there are any errors which provide related diagnostics + return lsp.RelatedFullDocumentDiagnosticReport( + items=items, + ) + + +@pyangls.feature(lsp.WORKSPACE_DIAGNOSTIC) +def workspace_diagnostic( + params: lsp.WorkspaceDiagnosticParams, +) -> lsp.WorkspaceDiagnosticReport: + """Returns diagnostic report.""" + # pyangls.show_message("Received Workspace Diagnostic") + if pyangls.client_capabilities.text_document is None or \ + pyangls.client_capabilities.text_document.diagnostic is None: + pyangls.show_message("Unexpected workspace/diagnostic from incapable client.") + + items : List[lsp.WorkspaceDocumentDiagnosticReport] = [] + for text_doc_uri in pyangls.workspace.text_documents.keys(): + text_doc = pyangls.workspace.get_text_document(text_doc_uri) + doc_items = _build_doc_diagnostics(text_doc.path) + if doc_items is not None: + items.append( + lsp.WorkspaceFullDocumentDiagnosticReport( + uri=text_doc.uri, + version=text_doc.version, + items=doc_items, + kind=lsp.DocumentDiagnosticReportKind.Full, + ) + ) + + return lsp.WorkspaceDiagnosticReport(items=items) + + +# pyang supports LSP TextDocumentSyncKind Full but not Incremental +# The mapping is provided via initialization parameters of pygls LanguageServer +@pyangls.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) +def did_change( + ls: LanguageServer, + params: lsp.DidChangeTextDocumentParams +): + """Text document did change notification.""" + # ls.show_message("Received Text Document Did Change") + _clear_ctx_validation() + for content_change in params.content_changes: + ls.workspace.update_text_document(params.text_document, content_change) + + _update_ctx_modules() + _validate_ctx_modules() + _publish_workspace_diagnostics() + + +@pyangls.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) +def did_close( + ls: PyangLanguageServer, + params: lsp.DidCloseTextDocumentParams +): + """Text document did close notification.""" + # ls.show_message("Received Text Document Did Close") + _clear_ctx_validation() + text_doc = ls.workspace.get_text_document(params.text_document.uri) + # Force file read on next source access + text_doc._source = None + + _update_ctx_modules() + _validate_ctx_modules() + _publish_workspace_diagnostics() + + +@pyangls.feature(lsp.TEXT_DOCUMENT_DID_OPEN) +def did_open( + ls: LanguageServer, + params: lsp.DidOpenTextDocumentParams +): + """Text document did open notification.""" + # ls.show_message("Received Text Document Did Open") + text_doc = ls.workspace.get_text_document(params.text_document.uri) + # Prevent direct file read on next TextDocument.source access + text_doc._source = params.text_document.text + # Keep Eglot+Flymake happy... without this buffer diagnostics are not shown + # in the mode-line even though diagnostics for the file is reported earlier + # via _publish_workspace_diagnostics + _publish_doc_diagnostics(text_doc) + + +@pyangls.feature(lsp.TEXT_DOCUMENT_FORMATTING) +def formatting( + ls: LanguageServer, + params: lsp.DocumentFormattingParams +): + """Text document formatting.""" + # ls.show_message("Received Text Document Formatting") + text_doc = ls.workspace.get_text_document(params.text_document.uri) + source = text_doc.source + + if source is None: + ls.show_message("No source found") + return [] + + module = _update_ctx_module(text_doc) + if module is None: + if _have_parser_errors(): + ls.show_message("Document was syntactically invalid. Did not format.") + return [] + + _validate_ctx_modules() + + fmt_text=_format_yang(source, params.options, module) + + start_pos = lsp.Position(line=0, character=0) + end_pos = lsp.Position(line=len(text_doc.lines), character=0) + text_range = lsp.Range(start=start_pos, end=end_pos) + + return [lsp.TextEdit(range=text_range, new_text=fmt_text)] diff --git a/pyang/plugins/lint.py b/pyang/plugins/lint.py index ff94bbdd0..3032e73b6 100644 --- a/pyang/plugins/lint.py +++ b/pyang/plugins/lint.py @@ -76,7 +76,8 @@ def _setup_ctx(self, ctx): ctx.strict = True ctx.canonical = True - ctx.max_identifier_len = 64 + if ctx.max_identifier_len is None: + ctx.max_identifier_len = 64 ctx.implicit_errors = False # always add additional prefixes given on the command line diff --git a/pyang/scripts/pyang_tool.py b/pyang/scripts/pyang_tool.py index 373b506ff..96ccece17 100755 --- a/pyang/scripts/pyang_tool.py +++ b/pyang/scripts/pyang_tool.py @@ -5,7 +5,6 @@ import optparse import io import shutil -import codecs from pathlib import Path import pyang @@ -15,8 +14,8 @@ from pyang import hello from pyang import context from pyang import repository -from pyang import statements from pyang import syntax +from pyang.lsp import server as pyangls def run(): @@ -41,6 +40,7 @@ def run(): fmts = {} xforms = {} + p : plugin.PyangPlugin for p in plugin.plugins: p.add_output_format(fmts) p.add_transform(xforms) @@ -131,6 +131,10 @@ def run(): dest="format", help="Convert to FORMAT. Supported formats " \ "are: " + ', '.join(fmts)), + optparse.make_option("-l", "--lsp", + dest="lsp", + action="store_true", + help="Run as LSP server instead of CLI tool."), optparse.make_option("-o", "--output", dest="outfile", help="Write the output to OUTFILE instead " \ @@ -212,18 +216,23 @@ def run(): action="store_true", help="Do not recurse into directories in the \ yang path."), + optparse.make_option("--no-env-path", + dest="no_env_path", + action="store_true", + help="Do not use environment for yang file paths."), ] optparser = optparse.OptionParser(usage, add_help_option = False) optparser.version = '%prog ' + pyang.__version__ optparser.add_options(optlist) + pyangls.add_opts(optparser) for p in plugin.plugins: p.add_opts(optparser) (o, args) = optparser.parse_args() - if o.outfile is not None and o.format is None: + if o.outfile is not None and o.format is None and o.lsp is None: sys.stderr.write("no format specified\n") sys.exit(1) @@ -252,7 +261,12 @@ def run(): else: path += os.pathsep + "." - repos = repository.FileRepository(path, no_path_recurse=o.no_path_recurse, + if o.no_env_path: + use_env = False + else: + use_env = True + repos = repository.FileRepository(path, use_env, + no_path_recurse=o.no_path_recurse, verbose=o.verbose) ctx = context.Context(repos) @@ -335,6 +349,17 @@ def run(): for p in plugin.plugins: p.pre_load_modules(ctx) + if o.lsp: + try: + pyangls.try_import_deps() + pyangls.start_server(o, ctx, fmts) + sys.exit(0) + except ModuleNotFoundError as e: + print("LSP feature required external dependencies are missing") + print(str(e)) + print("Please resolve dependencies to use pyang as an LSP server") + sys.exit(1) + exit_code = 0 modules = [] diff --git a/pyang/translators/yang.py b/pyang/translators/yang.py index 670b78b6a..7ff4be4a5 100644 --- a/pyang/translators/yang.py +++ b/pyang/translators/yang.py @@ -30,6 +30,11 @@ def add_opts(self, optparser): type="int", dest="yang_line_length", help="Maximum line length"), + optparse.make_option("--yang-indent-size", + dest="yang_indent_size", + default=2, + action="store_true", + help="Indentation size"), ] g = optparser.add_option_group("YANG output specific options") g.add_options(optlist) @@ -51,7 +56,7 @@ def emit_yang(ctx, module, fd): make_link_list(ctx, module, link_list) link_list['last'] = None - emit_stmt(ctx, module, fd, 0, None, None, False, '', ' ', link_list) + emit_stmt(ctx, module, fd, 0, None, None, False, '', link_list) # always add newline between keyword and argument _force_newline_arg = ('description', 'reference', 'contact', 'organization') @@ -153,11 +158,13 @@ def make_link_list(ctx, stmt, link_list): make_link_list(ctx, s, link_list) def emit_stmt(ctx, stmt, fd, level, prev_kwd, prev_kwd_class, islast, - indent, indentstep, link_list): + indent, link_list): if is_line_end_comment(stmt): # line end comments has been printed after last meaningful statement return + indentstep = ctx.opts.yang_indent_size * ' ' + if ctx.opts.yang_remove_unused_imports and stmt.keyword == 'import': for p in stmt.parent.i_unused_prefixes: if stmt.parent.i_unused_prefixes[p] == stmt: @@ -270,7 +277,7 @@ def emit_stmt(ctx, stmt, fd, level, prev_kwd, prev_kwd_class, islast, link_list['last'] = s emit_stmt(ctx, s, fd, level + 1, prev_kwd, kwd_class, i == len(substmts), - indent + (indentstep * n), indentstep, link_list) + indent + (indentstep * n), link_list) if not is_line_end_comment(s): kwd_class = get_kwd_class(s.keyword) prev_kwd = s.keyword diff --git a/pyang/yang_parser.py b/pyang/yang_parser.py index 238704e00..fd3ebfb9a 100644 --- a/pyang/yang_parser.py +++ b/pyang/yang_parser.py @@ -9,6 +9,18 @@ from . import statements from . import syntax +# List of YANG parser errors handled +errors = [ + 'EOF_ERROR', + 'SYNTAX_ERROR', + 'ILLEGAL_ESCAPE', + 'ILLEGAL_ESCAPE_WARN', + 'TRAILING_GARBAGE', + 'EXPECTED_ARGUMENT', + 'EXPECTED_QUOTED_STRING', + 'INCOMPLETE_STATEMENT', + ] + class YangTokenizer(object): def __init__(self, text, pos, errors, max_line_len=None, keep_comments=False, diff --git a/requirements.txt b/requirements.txt index 86c871ed7..ab90481d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -lxml \ No newline at end of file +lxml diff --git a/setup.py b/setup.py index 52b26c62e..cd48f6228 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ def run_commands(self): 'json2xml = pyang.scripts.json2xml:main', ] }, - packages=['pyang', 'pyang.plugins', 'pyang.scripts', 'pyang.translators', 'pyang.transforms'], + packages=['pyang', 'pyang.plugins', 'pyang.scripts', 'pyang.translators', 'pyang.transforms', 'pyang.lsp'], data_files=[ ('share/man/man1', man1), ('share/yang/modules/iana', modules_iana), diff --git a/test/test_lsp/Makefile b/test/test_lsp/Makefile new file mode 100644 index 000000000..72d7df522 --- /dev/null +++ b/test/test_lsp/Makefile @@ -0,0 +1,5 @@ +test: + ./client.py || exit 1; echo "ok"; + +clean: + rm -f test.log diff --git a/test/test_lsp/client.py b/test/test_lsp/client.py new file mode 100755 index 000000000..be85ef77f --- /dev/null +++ b/test/test_lsp/client.py @@ -0,0 +1,1593 @@ +#!/usr/bin/env python + +"""pyang Test LSP Client""" + +import asyncio +import json +import logging +import os +import shutil +import sys +import traceback +from typing import List +from pygls.lsp.client import BaseLanguageClient +from lsprotocol import types as lsp +from pygls.uris import from_fs_path, to_fs_path +from pygls.exceptions import PyglsError, JsonRpcException + +CLIENT_NAME = 'pyangtlc' +CLIENT_VERSION = 'v0.1' + +EDIT_FOLDER = 'edit' +WORKSPACE_FOLDER = 'workspace' +EXPECTATION_FOLDER = 'expect' + +class PyangTestLanguageClient(BaseLanguageClient): + def _build_workspace(self, folder) -> List[str]: + yang_files = [] + for root, _, files in os.walk(folder): + for file in files: + if file.endswith(".yang"): + yang_files.append(os.path.join(os.path.realpath('.'), + os.path.join(root, file))) + return yang_files + + def handle_server_error( + self, + error: Exception, + source: PyglsError | JsonRpcException + ) -> None: + logger.info(error) + logger.info(source) + + async def handle_server_exit( + self, + server: asyncio.subprocess.Process, + ) -> None: + logger.info(server) + + def __init__(self): + self.workspace = self._build_workspace(WORKSPACE_FOLDER) + self.test_step : int = 0 + self.report_server_error = self.handle_server_error + self.server_exit = self.handle_server_exit + super().__init__( + name=CLIENT_NAME, + version=CLIENT_VERSION + ) + +pyangtlc = PyangTestLanguageClient() + +logger = logging.getLogger() + +def _ensure_server_capabilities(caps: lsp.ServerCapabilities): + """Ensure reported capabilities have expected content""" + + # Ordered by pygls generated jsonrpc result sequence + + assert caps.position_encoding == lsp.PositionEncodingKind.Utf16 + + assert caps.text_document_sync is not None + assert type(caps.text_document_sync) is lsp.TextDocumentSyncOptions + assert caps.text_document_sync.open_close + assert caps.text_document_sync.change == lsp.TextDocumentSyncKind.Full + assert not caps.text_document_sync.will_save + assert not caps.text_document_sync.will_save_wait_until + assert not caps.text_document_sync.save + + assert caps.document_formatting_provider + + assert caps.execute_command_provider is not None + assert not caps.execute_command_provider.commands + + assert caps.diagnostic_provider is not None + assert caps.diagnostic_provider.inter_file_dependencies + assert caps.diagnostic_provider.workspace_diagnostics + assert caps.diagnostic_provider.identifier == 'pyang' + + assert caps.workspace is not None + assert caps.workspace.workspace_folders is not None + assert caps.workspace.workspace_folders.supported + assert caps.workspace.workspace_folders.change_notifications + assert caps.workspace.file_operations + assert caps.workspace.file_operations.did_create is None + assert caps.workspace.file_operations.did_delete is None + assert caps.workspace.file_operations.did_rename is None + assert caps.workspace.file_operations.will_create is None + assert caps.workspace.file_operations.will_delete is None + assert caps.workspace.file_operations.will_rename is None + +def _ensure_server_incapabilities(caps: lsp.ServerCapabilities): + """Ensure incapabilies are not reported at all""" + + # Ordered alphabetically + assert caps.call_hierarchy_provider is None + assert caps.code_action_provider is None + assert caps.code_lens_provider is None + assert caps.color_provider is None + assert caps.completion_provider is None + assert caps.declaration_provider is None + assert caps.definition_provider is None + assert caps.document_highlight_provider is None + assert caps.document_link_provider is None + assert caps.document_link_provider is None + assert caps.document_on_type_formatting_provider is None + assert caps.document_range_formatting_provider is None + assert caps.document_symbol_provider is None + assert caps.experimental is None + assert caps.folding_range_provider is None + assert caps.hover_provider is None + assert caps.implementation_provider is None + assert caps.inlay_hint_provider is None + assert caps.inline_completion_provider is None + assert caps.inline_value_provider is None + assert caps.linked_editing_range_provider is None + assert caps.moniker_provider is None + assert caps.notebook_document_sync is None + assert caps.references_provider is None + assert caps.rename_provider is None + assert caps.selection_range_provider is None + assert caps.semantic_tokens_provider is None + assert caps.signature_help_provider is None + assert caps.type_definition_provider is None + assert caps.type_hierarchy_provider is None + assert caps.workspace_symbol_provider is None + +def _validate_initialize_result(result: lsp.InitializeResult): + """Validate `initialize` result""" + + logger.info("Received initialize result. Validating...") + + caps = result.capabilities + _ensure_server_capabilities(caps) + _ensure_server_incapabilities(caps) + + assert result.server_info is not None + assert result.server_info.name == 'pyang' + assert result.server_info.version == 'v0.1' + + logger.info("Valid initialize result!") + +def _validate_diagnostics( + diagnostics: List[lsp.Diagnostic], + exp_filepath: str +): + with open(exp_filepath) as exp_file: + exp_json = json.load(exp_file) + exp_items = exp_json['items'] + try: + assert len(diagnostics) == len(exp_items) + i = 0 + for exp_item in exp_items: + diagnostic = diagnostics[i] + range = diagnostic.range + exp_start = exp_item['range']['start'] + exp_end = exp_item['range']['end'] + assert range.start.line == exp_start['line'] + assert range.start.character == exp_start['character'] + assert range.end.line == exp_end['line'] + assert range.end.character == exp_end['character'] + assert diagnostic.message == exp_item['message'] + assert diagnostic.severity == exp_item['severity'] + assert diagnostic.code == exp_item['code'] + assert diagnostic.source == exp_item['source'] + exp_tags = exp_item['tags'] + if exp_tags: + assert diagnostic.tags + assert len(diagnostic.tags) == len(exp_tags) + j = 0 + for exp_tag in exp_tags: + tag = diagnostic.tags[j] + assert tag == exp_tag + j += 1 + else: + assert not diagnostic.tags + exp_rel_infos = exp_item['relatedInformation'] + if exp_rel_infos: + assert diagnostic.related_information + assert len(diagnostic.related_information) == len(exp_rel_infos) + j = 0 + for exp_rel_info in exp_rel_infos: + rel_info = diagnostic.related_information[j] + uri = rel_info.location.uri + assert uri == exp_rel_info['location']['uri'] + range = rel_info.location.range + exp_start = exp_rel_info['location']['range']['start'] + exp_end = exp_rel_info['location']['range']['end'] + assert range.start.line == exp_start['line'] + assert range.start.character == exp_start['character'] + assert range.end.line == exp_end['line'] + assert range.end.character == exp_end['character'] + assert rel_info.message == exp_rel_info['message'] + j += 1 + else: + assert not diagnostic.related_information + i += 1 + except Exception as exc: + exp_file.close() + raise exc + exp_file.close() + +@pyangtlc.feature('window/logMessage') +async def window_log_message(ls, params: lsp.LogMessageParams): + logging.info("Received window/logMessage: %s", params.message) + +@pyangtlc.feature('window/showMessage') +async def window_show_message(ls, params: lsp.ShowMessageParams): + logging.info("Received window/showMessage: %s", params.message) + +def _file_in_workspace(uri: str) -> bool: + return to_fs_path(uri) in pyangtlc.workspace + +def _expected_diagnostics_filepath(uri: str) -> str: + fs_path = to_fs_path(uri) + assert fs_path is not None + exp_root, _ = os.path.splitext(fs_path) + exp_filename = os.path.basename(exp_root) + '.json' + return os.path.join(EXPECTATION_FOLDER, str(pyangtlc.test_step), exp_filename) + +@pyangtlc.feature('textDocument/publishDiagnostics') +async def document_publish_diagnostics(ls, params: lsp.PublishDiagnosticsParams): + logging.info("Received textDocument/publishDiagnostics %s. Validating...", params.uri) + assert _file_in_workspace(params.uri) + try: + exp_diag_file = _expected_diagnostics_filepath(params.uri) + _validate_diagnostics(params.diagnostics, exp_diag_file) + except Exception: + traceback.print_exc() + logger.error("Invalid textDocument/publishDiagnostics! %s", params.uri) + sys.exit(1) + + logger.info("Valid textDocument/publishDiagnostics %s!", params.uri) + +async def test_generic(): + await asyncio.wait_for( + pyangtlc.start_io('pyang','--lsp'), + timeout=2.0 + ) + + pyangtlc.test_step = 1 + + test_doc = 'test-a.yang' + test_doc_path = os.path.join(os.path.realpath(WORKSPACE_FOLDER), test_doc) + doc_uri = from_fs_path(test_doc_path) + assert doc_uri is not None + with open(test_doc_path) as test_fd: + doc_source = test_fd.read() + test_fd.close() + + def prepare_client_capabilities() -> lsp.ClientCapabilities: + """Prepare reference client capabilities based on a generic LSP 3.17 client""" + + execute_command_caps = lsp.ExecuteCommandClientCapabilities( + dynamic_registration=False, + ) + workspace_edit_caps = lsp.WorkspaceEditClientCapabilities( + document_changes=True, + ) + did_change_watched_files_caps = lsp.DidChangeWatchedFilesClientCapabilities( + dynamic_registration=True, + ) + symbol_caps = lsp.WorkspaceSymbolClientCapabilities( + dynamic_registration=False, + ) + workspace_caps = lsp.WorkspaceClientCapabilities( + apply_edit=True, + execute_command=execute_command_caps, + workspace_edit=workspace_edit_caps, + did_change_watched_files=did_change_watched_files_caps, + symbol=symbol_caps, + configuration=True, + workspace_folders=True, + ) + + synchronization_caps = lsp.TextDocumentSyncClientCapabilities( + dynamic_registration=False, + will_save=True, + will_save_wait_until=True, + did_save=True, + ) + completion_item_resolve_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeResolveSupportType( + [ + 'documentation', + 'details', + 'additionalTextEdits' + ] + ) + completion_item_tag_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeTagSupportType( + [ + lsp.CompletionItemTag.Deprecated + ] + ) + completion_item_caps = lsp.CompletionClientCapabilitiesCompletionItemType( + snippet_support=True, + deprecated_support=True, + resolve_support=completion_item_resolve_support_caps, + tag_support=completion_item_tag_support_caps, + ) + completion_caps = lsp.CompletionClientCapabilities( + dynamic_registration=False, + completion_item=completion_item_caps, + context_support=True, + ) + content_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + hover_caps = lsp.HoverClientCapabilities( + dynamic_registration=False, + content_format=content_format_caps, + ) + parameter_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationTypeParameterInformationType( + label_offset_support=True, + ) + document_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + signature_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationType( + parameter_information=parameter_information_caps, + documentation_format=document_format_caps, + active_parameter_support=True, + ) + signature_help_caps = lsp.SignatureHelpClientCapabilities( + dynamic_registration=False, + signature_information=signature_information_caps, + ) + references_caps = lsp.ReferenceClientCapabilities( + dynamic_registration=False, + ) + definition_caps = lsp.DefinitionClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + declaration_caps = lsp.DeclarationClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + implementation_caps = lsp.ImplementationClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + type_definition_caps = lsp.TypeDefinitionClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + symbol_kind_caps = lsp.DocumentSymbolClientCapabilitiesSymbolKindType( + [ + lsp.SymbolKind.File, + lsp.SymbolKind.Module, + lsp.SymbolKind.Namespace, + lsp.SymbolKind.Package, + lsp.SymbolKind.Class, + lsp.SymbolKind.Method, + lsp.SymbolKind.Property, + lsp.SymbolKind.Field, + lsp.SymbolKind.Constructor, + lsp.SymbolKind.Enum, + lsp.SymbolKind.Interface, + lsp.SymbolKind.Function, + lsp.SymbolKind.Variable, + lsp.SymbolKind.Constant, + lsp.SymbolKind.String, + lsp.SymbolKind.Number, + lsp.SymbolKind.Boolean, + lsp.SymbolKind.Array, + lsp.SymbolKind.Object, + lsp.SymbolKind.Key, + lsp.SymbolKind.Null, + lsp.SymbolKind.EnumMember, + lsp.SymbolKind.Struct, + lsp.SymbolKind.Event, + lsp.SymbolKind.Operator, + lsp.SymbolKind.TypeParameter, + ] + ) + document_symbol_caps = lsp.DocumentSymbolClientCapabilities( + dynamic_registration=False, + hierarchical_document_symbol_support=True, + symbol_kind=symbol_kind_caps, + ) + document_highlight_caps = lsp.DocumentHighlightClientCapabilities( + dynamic_registration=False, + ) + code_action_resolve_support_caps = lsp.CodeActionClientCapabilitiesResolveSupportType( + [ + 'edit', + 'command' + ] + ) + code_action_kind_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportTypeCodeActionKindType( + value_set=[ + lsp.CodeActionKind.QuickFix, + lsp.CodeActionKind.Refactor, + lsp.CodeActionKind.RefactorExtract, + lsp.CodeActionKind.RefactorInline, + lsp.CodeActionKind.RefactorRewrite, + lsp.CodeActionKind.Source, + lsp.CodeActionKind.SourceOrganizeImports, + ] + ) + code_action_literal_support_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportType( + code_action_kind=code_action_kind_caps, + ) + code_action_caps = lsp.CodeActionClientCapabilities( + dynamic_registration=False, + resolve_support=code_action_resolve_support_caps, + data_support=True, + code_action_literal_support=code_action_literal_support_caps, + is_preferred_support=True, + ) + formatting_caps = lsp.DocumentFormattingClientCapabilities( + dynamic_registration=False, + ) + range_formatting_caps = lsp.DocumentRangeFormattingClientCapabilities( + dynamic_registration=False, + ) + rename_caps = lsp.RenameClientCapabilities( + dynamic_registration=False, + ) + inlay_hint_caps = lsp.InlayHintClientCapabilities( + dynamic_registration=False, + ) + publish_diagnostics_tag_support_caps = lsp.PublishDiagnosticsClientCapabilitiesTagSupportType( + [ + lsp.DiagnosticTag.Unnecessary, + lsp.DiagnosticTag.Deprecated, + ] + ) + publish_diagnostics_caps = lsp.PublishDiagnosticsClientCapabilities( + related_information=False, + code_description_support=False, + tag_support=publish_diagnostics_tag_support_caps, + ) + diagnostic_caps = lsp.DiagnosticClientCapabilities( + dynamic_registration=False, + related_document_support=True, + ) + text_document_caps = lsp.TextDocumentClientCapabilities( + synchronization=synchronization_caps, + completion=completion_caps, + hover=hover_caps, + signature_help=signature_help_caps, + references=references_caps, + definition=definition_caps, + declaration=declaration_caps, + implementation=implementation_caps, + type_definition=type_definition_caps, + document_symbol=document_symbol_caps, + document_highlight=document_highlight_caps, + code_action=code_action_caps, + formatting=formatting_caps, + range_formatting=range_formatting_caps, + rename=rename_caps, + inlay_hint=inlay_hint_caps, + publish_diagnostics=publish_diagnostics_caps, + diagnostic=diagnostic_caps, + ) + + show_document_caps = lsp.ShowDocumentClientCapabilities( + support=True, + ) + window_caps = lsp.WindowClientCapabilities( + show_document=show_document_caps, + work_done_progress=True, + ) + + position_encoding_caps = [ + lsp.PositionEncodingKind.Utf32, + lsp.PositionEncodingKind.Utf8, + lsp.PositionEncodingKind.Utf16, + ] + + general_caps = lsp.GeneralClientCapabilities( + position_encodings=position_encoding_caps, # type: ignore + ) + + return lsp.ClientCapabilities( + workspace=workspace_caps, + text_document=text_document_caps, + window=window_caps, + general=general_caps, + ) + + client_capabilities = prepare_client_capabilities() + + client_info = lsp.InitializeParamsClientInfoType( + name=CLIENT_NAME, + version=CLIENT_VERSION, + ) + + root_path = os.path.realpath(WORKSPACE_FOLDER) + root_uri = from_fs_path(root_path) + assert root_uri is not None + workspace_folders = [ + lsp.WorkspaceFolder( + uri=root_uri, + name=os.path.basename(root_path) + ) + ] + + initialize_params = lsp.InitializeParams( + capabilities=client_capabilities, + client_info=client_info, + root_path=root_path, + root_uri=root_uri, + workspace_folders=workspace_folders, + ) + + initialize_result = await asyncio.wait_for( + pyangtlc.initialize_async( + params=initialize_params, + ), + timeout=2.0 + ) + _validate_initialize_result(initialize_result) + + did_change_configuration_params = lsp.DidChangeConfigurationParams( + settings=None, + ) + pyangtlc.workspace_did_change_configuration( + params=did_change_configuration_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + workspace_diagnostic_params = lsp.WorkspaceDiagnosticParams( + previous_result_ids=[], + ) + def validate_workspace_diagnostic_result(result: lsp.WorkspaceDiagnosticReport): + """Validate `workspace/diagnostic` result""" + + logger.info("Received workspace/diagnostic result. Validating...") + for item in result.items: + assert item.kind == lsp.DocumentDiagnosticReportKind.Full + _validate_diagnostics(item.items, _expected_diagnostics_filepath(item.uri)) # type: ignore + logger.info("Valid workspace/diagnostic result %s!", item.uri) + workspace_diagnostic_result = await asyncio.wait_for( + pyangtlc.workspace_diagnostic_async( + params=workspace_diagnostic_params, + ), + timeout=1.0 + ) + validate_workspace_diagnostic_result(workspace_diagnostic_result) + + document_diagnostic_params = lsp.DocumentDiagnosticParams( + text_document=lsp.TextDocumentIdentifier( + uri=doc_uri + ), + ) + def validate_document_diagnostic_result(result: lsp.RelatedFullDocumentDiagnosticReport + | lsp.RelatedUnchangedDocumentDiagnosticReport): + """Validate `textDocument/diagnostic` result""" + + logger.info("Received textDocument/diagnostic result. Validating...") + assert result.kind == lsp.DocumentDiagnosticReportKind.Full + diagnostics = result.items # type: ignore + try: + _validate_diagnostics(diagnostics, _expected_diagnostics_filepath(doc_uri)) + except AssertionError as err: + traceback.print_exc() + logger.error("Invalid textDocument/diagnostic result! " + str(err)) + sys.exit(1) + + logger.info("Valid textDocument/diagnostic result %s!", doc_uri) + document_diagnostic_result = await asyncio.wait_for( + pyangtlc.text_document_diagnostic_async( + params=document_diagnostic_params, + ), + timeout=1.0 + ) + validate_document_diagnostic_result(document_diagnostic_result) + + document_formatting_params = lsp.DocumentFormattingParams( + text_document=lsp.TextDocumentIdentifier( + uri=doc_uri + ), + options=lsp.FormattingOptions( + tab_size=4, + insert_spaces=True + ), + ) + def validate_document_formatting_result(result: List[lsp.TextEdit] | None): + """Validate `textDocument/formatting` result""" + + logger.info("Received textDocument/formatting result. Validating...") + assert result + assert len(result) == 1 + fmt_text = result[0].new_text + # logger.debug("Received formatted text:\n" + fmt_text) + exp_file = open('expect/test-a.yang') + exp_text = exp_file.read() + exp_file.close() + assert fmt_text == exp_text + logger.info("Valid textDocument/formatting result!") + document_formatting_result = await asyncio.wait_for( + pyangtlc.text_document_formatting_async( + params=document_formatting_params, + ), + timeout=1.0 + ) + validate_document_formatting_result(document_formatting_result) + + await asyncio.wait_for( + pyangtlc.shutdown_async( + params=None + ), + timeout=1.0 + ) + + pyangtlc.exit( + params=None + ) + +async def test_eglot(): + await asyncio.wait_for( + pyangtlc.start_io('pyang','--lsp'), + timeout=2.0 + ) + + pyangtlc.test_step = 0 + + def prepare_client_capabilities() -> lsp.ClientCapabilities: + """Prepare reference client capabilities based on GNU Emacs Eglot 1.17""" + + execute_command_caps = lsp.ExecuteCommandClientCapabilities( + dynamic_registration=False, + ) + workspace_edit_caps = lsp.WorkspaceEditClientCapabilities( + document_changes=True, + ) + did_change_watched_files_caps = lsp.DidChangeWatchedFilesClientCapabilities( + dynamic_registration=True, + ) + symbol_caps = lsp.WorkspaceSymbolClientCapabilities( + dynamic_registration=False, + ) + workspace_caps = lsp.WorkspaceClientCapabilities( + apply_edit=True, + execute_command=execute_command_caps, + workspace_edit=workspace_edit_caps, + did_change_watched_files=did_change_watched_files_caps, + symbol=symbol_caps, + configuration=True, + workspace_folders=True, + ) + + synchronization_caps = lsp.TextDocumentSyncClientCapabilities( + dynamic_registration=False, + will_save=True, + will_save_wait_until=True, + did_save=True, + ) + completion_item_resolve_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeResolveSupportType( + [ + 'documentation', + 'details', + 'additionalTextEdits' + ] + ) + completion_item_tag_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeTagSupportType( + [ + lsp.CompletionItemTag.Deprecated + ] + ) + completion_item_caps = lsp.CompletionClientCapabilitiesCompletionItemType( + snippet_support=True, + deprecated_support=True, + resolve_support=completion_item_resolve_support_caps, + tag_support=completion_item_tag_support_caps, + ) + completion_caps = lsp.CompletionClientCapabilities( + dynamic_registration=False, + completion_item=completion_item_caps, + context_support=True, + ) + content_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + hover_caps = lsp.HoverClientCapabilities( + dynamic_registration=False, + content_format=content_format_caps, + ) + parameter_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationTypeParameterInformationType( + label_offset_support=True, + ) + document_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + signature_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationType( + parameter_information=parameter_information_caps, + documentation_format=document_format_caps, + active_parameter_support=True, + ) + signature_help_caps = lsp.SignatureHelpClientCapabilities( + dynamic_registration=False, + signature_information=signature_information_caps, + ) + references_caps = lsp.ReferenceClientCapabilities( + dynamic_registration=False, + ) + definition_caps = lsp.DefinitionClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + declaration_caps = lsp.DeclarationClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + implementation_caps = lsp.ImplementationClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + type_definition_caps = lsp.TypeDefinitionClientCapabilities( + dynamic_registration=False, + link_support=True, + ) + symbol_kind_caps = lsp.DocumentSymbolClientCapabilitiesSymbolKindType( + [ + lsp.SymbolKind.File, + lsp.SymbolKind.Module, + lsp.SymbolKind.Namespace, + lsp.SymbolKind.Package, + lsp.SymbolKind.Class, + lsp.SymbolKind.Method, + lsp.SymbolKind.Property, + lsp.SymbolKind.Field, + lsp.SymbolKind.Constructor, + lsp.SymbolKind.Enum, + lsp.SymbolKind.Interface, + lsp.SymbolKind.Function, + lsp.SymbolKind.Variable, + lsp.SymbolKind.Constant, + lsp.SymbolKind.String, + lsp.SymbolKind.Number, + lsp.SymbolKind.Boolean, + lsp.SymbolKind.Array, + lsp.SymbolKind.Object, + lsp.SymbolKind.Key, + lsp.SymbolKind.Null, + lsp.SymbolKind.EnumMember, + lsp.SymbolKind.Struct, + lsp.SymbolKind.Event, + lsp.SymbolKind.Operator, + lsp.SymbolKind.TypeParameter, + ] + ) + document_symbol_caps = lsp.DocumentSymbolClientCapabilities( + dynamic_registration=False, + hierarchical_document_symbol_support=True, + symbol_kind=symbol_kind_caps, + ) + document_highlight_caps = lsp.DocumentHighlightClientCapabilities( + dynamic_registration=False, + ) + code_action_resolve_support_caps = lsp.CodeActionClientCapabilitiesResolveSupportType( + [ + 'edit', + 'command' + ] + ) + code_action_kind_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportTypeCodeActionKindType( + value_set=[ + lsp.CodeActionKind.QuickFix, + lsp.CodeActionKind.Refactor, + lsp.CodeActionKind.RefactorExtract, + lsp.CodeActionKind.RefactorInline, + lsp.CodeActionKind.RefactorRewrite, + lsp.CodeActionKind.Source, + lsp.CodeActionKind.SourceOrganizeImports, + ] + ) + code_action_literal_support_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportType( + code_action_kind=code_action_kind_caps, + ) + code_action_caps = lsp.CodeActionClientCapabilities( + dynamic_registration=False, + resolve_support=code_action_resolve_support_caps, + data_support=True, + code_action_literal_support=code_action_literal_support_caps, + is_preferred_support=True, + ) + formatting_caps = lsp.DocumentFormattingClientCapabilities( + dynamic_registration=False, + ) + range_formatting_caps = lsp.DocumentRangeFormattingClientCapabilities( + dynamic_registration=False, + ) + rename_caps = lsp.RenameClientCapabilities( + dynamic_registration=False, + ) + inlay_hint_caps = lsp.InlayHintClientCapabilities( + dynamic_registration=False, + ) + publish_diagnostics_tag_support_caps = lsp.PublishDiagnosticsClientCapabilitiesTagSupportType( + [ + lsp.DiagnosticTag.Unnecessary, + lsp.DiagnosticTag.Deprecated, + ] + ) + publish_diagnostics_caps = lsp.PublishDiagnosticsClientCapabilities( + related_information=False, + code_description_support=False, + tag_support=publish_diagnostics_tag_support_caps, + ) + text_document_caps = lsp.TextDocumentClientCapabilities( + synchronization=synchronization_caps, + completion=completion_caps, + hover=hover_caps, + signature_help=signature_help_caps, + references=references_caps, + definition=definition_caps, + declaration=declaration_caps, + implementation=implementation_caps, + type_definition=type_definition_caps, + document_symbol=document_symbol_caps, + document_highlight=document_highlight_caps, + code_action=code_action_caps, + formatting=formatting_caps, + range_formatting=range_formatting_caps, + rename=rename_caps, + inlay_hint=inlay_hint_caps, + publish_diagnostics=publish_diagnostics_caps, + ) + + show_document_caps = lsp.ShowDocumentClientCapabilities( + support=True, + ) + window_caps = lsp.WindowClientCapabilities( + show_document=show_document_caps, + work_done_progress=True, + ) + + position_encoding_caps : List[lsp.PositionEncodingKind | str] | None = [ + lsp.PositionEncodingKind.Utf32, + lsp.PositionEncodingKind.Utf8, + lsp.PositionEncodingKind.Utf16, + ] + + general_caps = lsp.GeneralClientCapabilities( + position_encodings=position_encoding_caps, + ) + + return lsp.ClientCapabilities( + workspace=workspace_caps, + text_document=text_document_caps, + window=window_caps, + general=general_caps, + ) + + client_capabilities = prepare_client_capabilities() + + client_info = lsp.InitializeParamsClientInfoType( + name=CLIENT_NAME, + version=CLIENT_VERSION, + ) + + root_path = os.path.realpath(WORKSPACE_FOLDER) + root_uri = from_fs_path(root_path) + assert root_uri is not None + workspace_folders = [ + lsp.WorkspaceFolder( + uri=root_uri, + name=os.path.basename(root_path) + ) + ] + + initialize_params = lsp.InitializeParams( + capabilities=client_capabilities, + client_info=client_info, + root_path=root_path, + root_uri=root_uri, + workspace_folders=workspace_folders, + ) + initialize_result = await asyncio.wait_for( + pyangtlc.initialize_async( + params=initialize_params, + ), + timeout=2.0 + ) + _validate_initialize_result(initialize_result) + + test_doc = 'test-a.yang' + test_doc_path = os.path.join(os.path.realpath(WORKSPACE_FOLDER), test_doc) + doc_uri = from_fs_path(test_doc_path) + assert doc_uri is not None + with open(test_doc_path) as test_fd: + doc_source = test_fd.read() + test_fd.close() + + did_open_text_document_params = lsp.DidOpenTextDocumentParams( + text_document=lsp.TextDocumentItem( + uri=doc_uri, + language_id='yang', + version=0, + text=doc_source, + ) + ) + pyangtlc.text_document_did_open( + params=did_open_text_document_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + pyangtlc.test_step += 1 + + did_change_configuration_params = lsp.DidChangeConfigurationParams( + settings=None, + ) + pyangtlc.workspace_did_change_configuration( + params=did_change_configuration_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + document_formatting_params = lsp.DocumentFormattingParams( + text_document=lsp.TextDocumentIdentifier( + uri=doc_uri + ), + options=lsp.FormattingOptions( + tab_size=4, + insert_spaces=True + ), + ) + def validate_document_formatting_result(result: List[lsp.TextEdit] | None): + """Validate `textDocument/formatting` result""" + + logger.info("Received textDocument/formatting result. Validating...") + assert result + assert len(result) == 1 + fmt_text = result[0].new_text + # logger.debug("Received formatted text:\n" + fmt_text) + exp_file = open('expect/test-a.yang') + exp_text = exp_file.read() + exp_file.close() + assert fmt_text == exp_text + logger.info("Valid textDocument/formatting result!") + document_formatting_result = await asyncio.wait_for( + pyangtlc.text_document_formatting_async( + params=document_formatting_params, + ), + timeout=1.0 + ) + validate_document_formatting_result(document_formatting_result) + + await asyncio.wait_for( + pyangtlc.shutdown_async( + params=None + ), + timeout=1.0 + ) + + pyangtlc.exit( + params=None + ) + +async def test_vscode(): + await asyncio.wait_for( + pyangtlc.start_io('pyang','--lsp'), + timeout=2.0 + ) + + pyangtlc.test_step = 1 + + def prepare_client_capabilities() -> lsp.ClientCapabilities: + """Prepare reference client capabilities based on VS Code 1.88.1 """ + + resource_operations_caps = [ + lsp.ResourceOperationKind.Create, + lsp.ResourceOperationKind.Rename, + lsp.ResourceOperationKind.Delete, + ] + change_annotation_support_caps = lsp.WorkspaceEditClientCapabilitiesChangeAnnotationSupportType( + groups_on_label=True + ) + workspace_edit_caps = lsp.WorkspaceEditClientCapabilities( + document_changes=True, + resource_operations=resource_operations_caps, + failure_handling=lsp.FailureHandlingKind.TextOnlyTransactional, + normalizes_line_endings=True, + change_annotation_support=change_annotation_support_caps, + ) + did_change_configuration_caps = lsp.DidChangeConfigurationClientCapabilities( + dynamic_registration=True, + ) + did_change_watched_files_caps = lsp.DidChangeWatchedFilesClientCapabilities( + dynamic_registration=True, + ) + workspace_symbol_kind_caps = lsp.WorkspaceSymbolClientCapabilitiesSymbolKindType( + value_set=[ + lsp.SymbolKind.File, + lsp.SymbolKind.Module, + lsp.SymbolKind.Namespace, + lsp.SymbolKind.Package, + lsp.SymbolKind.Class, + lsp.SymbolKind.Method, + lsp.SymbolKind.Property, + lsp.SymbolKind.Field, + lsp.SymbolKind.Constructor, + lsp.SymbolKind.Enum, + lsp.SymbolKind.Interface, + lsp.SymbolKind.Function, + lsp.SymbolKind.Variable, + lsp.SymbolKind.Constant, + lsp.SymbolKind.String, + lsp.SymbolKind.Number, + lsp.SymbolKind.Boolean, + lsp.SymbolKind.Array, + lsp.SymbolKind.Object, + lsp.SymbolKind.Key, + lsp.SymbolKind.Null, + lsp.SymbolKind.EnumMember, + lsp.SymbolKind.Struct, + lsp.SymbolKind.Event, + lsp.SymbolKind.Operator, + lsp.SymbolKind.TypeParameter, + ] + ) + workspace_tag_support_caps = lsp.WorkspaceSymbolClientCapabilitiesTagSupportType( + value_set=[ + lsp.SymbolTag.Deprecated + ] + ) + symbol_caps = lsp.WorkspaceSymbolClientCapabilities( + dynamic_registration=True, + symbol_kind=workspace_symbol_kind_caps, + tag_support=workspace_tag_support_caps, + ) + code_lens_caps = lsp.CodeLensWorkspaceClientCapabilities( + refresh_support=True, + ) + execute_command_caps = lsp.ExecuteCommandClientCapabilities( + dynamic_registration=True, + ) + semantic_tokens_caps = lsp.SemanticTokensWorkspaceClientCapabilities( + refresh_support=True, + ) + file_operations_caps = lsp.FileOperationClientCapabilities( + dynamic_registration=True, + did_create=True, + did_rename=True, + did_delete=True, + will_create=True, + will_rename=True, + will_delete=True, + ) + workspace_caps = lsp.WorkspaceClientCapabilities( + apply_edit=True, + workspace_edit=workspace_edit_caps, + did_change_configuration=did_change_configuration_caps, + did_change_watched_files=did_change_watched_files_caps, + symbol=symbol_caps, + code_lens=code_lens_caps, + execute_command=execute_command_caps, + configuration=True, + workspace_folders=True, + semantic_tokens=semantic_tokens_caps, + file_operations=file_operations_caps, + ) + + publish_diagnostics_tag_support_caps = lsp.PublishDiagnosticsClientCapabilitiesTagSupportType( + [ + lsp.DiagnosticTag.Unnecessary, + lsp.DiagnosticTag.Deprecated, + ] + ) + publish_diagnostics_caps = lsp.PublishDiagnosticsClientCapabilities( + related_information=True, + version_support=False, + tag_support=publish_diagnostics_tag_support_caps, + code_description_support=True, + data_support=True, + ) + synchronization_caps = lsp.TextDocumentSyncClientCapabilities( + dynamic_registration=True, + will_save=True, + will_save_wait_until=True, + did_save=True, + ) + documentation_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + completion_item_resolve_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeResolveSupportType( + [ + 'documentation', + 'detail', + 'additionalTextEdits' + ] + ) + completion_item_tag_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeTagSupportType( + [ + lsp.CompletionItemTag.Deprecated + ] + ) + insert_text_mode_support_caps = lsp.CompletionClientCapabilitiesCompletionItemTypeInsertTextModeSupportType( + value_set=[ + lsp.InsertTextMode.AsIs, + lsp.InsertTextMode.AdjustIndentation, + ] + ) + completion_item_caps = lsp.CompletionClientCapabilitiesCompletionItemType( + snippet_support=True, + commit_characters_support=True, + documentation_format=documentation_format_caps, + deprecated_support=True, + preselect_support=True, + tag_support=completion_item_tag_support_caps, + insert_replace_support=True, + resolve_support=completion_item_resolve_support_caps, + insert_text_mode_support=insert_text_mode_support_caps, + ) + completion_item_kind_caps = lsp.CompletionClientCapabilitiesCompletionItemKindType( + value_set=[ + lsp.CompletionItemKind.Text, + lsp.CompletionItemKind.Method, + lsp.CompletionItemKind.Function, + lsp.CompletionItemKind.Constructor, + lsp.CompletionItemKind.Field, + lsp.CompletionItemKind.Variable, + lsp.CompletionItemKind.Class, + lsp.CompletionItemKind.Interface, + lsp.CompletionItemKind.Module, + lsp.CompletionItemKind.Property, + lsp.CompletionItemKind.Unit, + lsp.CompletionItemKind.Value, + lsp.CompletionItemKind.Enum, + lsp.CompletionItemKind.Keyword, + lsp.CompletionItemKind.Snippet, + lsp.CompletionItemKind.Color, + lsp.CompletionItemKind.File, + lsp.CompletionItemKind.Reference, + lsp.CompletionItemKind.Folder, + lsp.CompletionItemKind.EnumMember, + lsp.CompletionItemKind.Constant, + lsp.CompletionItemKind.Struct, + lsp.CompletionItemKind.Event, + lsp.CompletionItemKind.Operator, + lsp.CompletionItemKind.TypeParameter, + ] + ) + completion_caps = lsp.CompletionClientCapabilities( + dynamic_registration=True, + context_support=True, + completion_item=completion_item_caps, + completion_item_kind=completion_item_kind_caps, + ) + content_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + hover_caps = lsp.HoverClientCapabilities( + dynamic_registration=True, + content_format=content_format_caps, + ) + document_format_caps = [ + lsp.MarkupKind.Markdown, + lsp.MarkupKind.PlainText, + ] + parameter_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationTypeParameterInformationType( + label_offset_support=True, + ) + signature_information_caps = lsp.SignatureHelpClientCapabilitiesSignatureInformationType( + documentation_format=document_format_caps, + parameter_information=parameter_information_caps, + active_parameter_support=True, + ) + signature_help_caps = lsp.SignatureHelpClientCapabilities( + dynamic_registration=True, + signature_information=signature_information_caps, + context_support=True, + ) + definition_caps = lsp.DefinitionClientCapabilities( + dynamic_registration=True, + link_support=True, + ) + references_caps = lsp.ReferenceClientCapabilities( + dynamic_registration=True, + ) + document_highlight_caps = lsp.DocumentHighlightClientCapabilities( + dynamic_registration=True, + ) + document_symbol_kind_caps = lsp.DocumentSymbolClientCapabilitiesSymbolKindType( + [ + lsp.SymbolKind.File, + lsp.SymbolKind.Module, + lsp.SymbolKind.Namespace, + lsp.SymbolKind.Package, + lsp.SymbolKind.Class, + lsp.SymbolKind.Method, + lsp.SymbolKind.Property, + lsp.SymbolKind.Field, + lsp.SymbolKind.Constructor, + lsp.SymbolKind.Enum, + lsp.SymbolKind.Interface, + lsp.SymbolKind.Function, + lsp.SymbolKind.Variable, + lsp.SymbolKind.Constant, + lsp.SymbolKind.String, + lsp.SymbolKind.Number, + lsp.SymbolKind.Boolean, + lsp.SymbolKind.Array, + lsp.SymbolKind.Object, + lsp.SymbolKind.Key, + lsp.SymbolKind.Null, + lsp.SymbolKind.EnumMember, + lsp.SymbolKind.Struct, + lsp.SymbolKind.Event, + lsp.SymbolKind.Operator, + lsp.SymbolKind.TypeParameter, + ] + ) + document_symbol_tag_support_caps = lsp.DocumentSymbolClientCapabilitiesTagSupportType( + value_set=[ + lsp.SymbolTag.Deprecated, + ] + ) + document_symbol_caps = lsp.DocumentSymbolClientCapabilities( + dynamic_registration=True, + symbol_kind=document_symbol_kind_caps, + hierarchical_document_symbol_support=True, + tag_support=document_symbol_tag_support_caps, + label_support=True, + ) + code_action_resolve_support_caps = lsp.CodeActionClientCapabilitiesResolveSupportType( + [ + 'edit' + ] + ) + code_action_kind_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportTypeCodeActionKindType( + value_set=[ + '', + lsp.CodeActionKind.QuickFix, + lsp.CodeActionKind.Refactor, + lsp.CodeActionKind.RefactorExtract, + lsp.CodeActionKind.RefactorInline, + lsp.CodeActionKind.RefactorRewrite, + lsp.CodeActionKind.Source, + lsp.CodeActionKind.SourceOrganizeImports, + ] + ) + code_action_literal_support_caps = lsp.CodeActionClientCapabilitiesCodeActionLiteralSupportType( + code_action_kind=code_action_kind_caps, + ) + code_action_caps = lsp.CodeActionClientCapabilities( + dynamic_registration=True, + is_preferred_support=True, + disabled_support=True, + data_support=True, + resolve_support=code_action_resolve_support_caps, + code_action_literal_support=code_action_literal_support_caps, + honors_change_annotations=False, + ) + code_lens_caps = lsp.CodeLensClientCapabilities( + dynamic_registration=True, + ) + formatting_caps = lsp.DocumentFormattingClientCapabilities( + dynamic_registration=True, + ) + range_formatting_caps = lsp.DocumentRangeFormattingClientCapabilities( + dynamic_registration=True, + ) + on_type_formatting_caps = lsp.DocumentOnTypeFormattingClientCapabilities( + dynamic_registration=True, + ) + rename_caps = lsp.RenameClientCapabilities( + dynamic_registration=True, + prepare_support=True, + prepare_support_default_behavior=lsp.PrepareSupportDefaultBehavior.Identifier, + honors_change_annotations=True, + ) + document_link_caps = lsp.DocumentLinkClientCapabilities( + dynamic_registration=True, + tooltip_support=True, + ) + type_definition_caps = lsp.TypeDefinitionClientCapabilities( + dynamic_registration=True, + link_support=True, + ) + implementation_caps = lsp.ImplementationClientCapabilities( + dynamic_registration=True, + link_support=True, + ) + color_provider_caps = lsp.DocumentColorClientCapabilities( + dynamic_registration=True, + ) + folding_range_caps = lsp.FoldingRangeClientCapabilities( + dynamic_registration=True, + range_limit=5000, + line_folding_only=True, + ) + declaration_caps = lsp.DeclarationClientCapabilities( + dynamic_registration=True, + link_support=True, + ) + selection_range_caps = lsp.SelectionRangeClientCapabilities( + dynamic_registration=True + ) + call_hierarchy_caps = lsp.CallHierarchyClientCapabilities( + dynamic_registration=True, + ) + semantic_tokens_requests_caps = lsp.SemanticTokensClientCapabilitiesRequestsType( + range=True, + full=lsp.SemanticTokensClientCapabilitiesRequestsTypeFullType1(delta=True), + ) + semantic_tokens_caps = lsp.SemanticTokensClientCapabilities( + dynamic_registration=True, + token_types=[ + 'namespace', + 'type', + 'class', + 'enum', + 'interface', + 'struct', + 'typeParameter', + 'parameter', + 'variable', + 'property', + 'enumMember', + 'event', + 'function', + 'method', + 'macro', + 'keyword', + 'modifier', + 'comment', + 'string', + 'number', + 'regexp', + 'operator', + ], + token_modifiers=[ + 'declaration', + 'definition', + 'readonly', + 'static', + 'deprecated', + 'abstract', + 'async', + 'modification', + 'documentation', + 'defaultLibrary', + ], + formats=[ + lsp.TokenFormat.Relative, + ], + requests=semantic_tokens_requests_caps, + multiline_token_support=False, + overlapping_token_support=False, + ) + linked_editing_range_caps = lsp.LinkedEditingRangeClientCapabilities( + dynamic_registration=True, + ) + text_document_caps = lsp.TextDocumentClientCapabilities( + publish_diagnostics=publish_diagnostics_caps, + synchronization=synchronization_caps, + completion=completion_caps, + hover=hover_caps, + signature_help=signature_help_caps, + definition=definition_caps, + references=references_caps, + document_highlight=document_highlight_caps, + document_symbol=document_symbol_caps, + code_action=code_action_caps, + code_lens=code_lens_caps, + formatting=formatting_caps, + range_formatting=range_formatting_caps, + on_type_formatting=on_type_formatting_caps, + rename=rename_caps, + document_link=document_link_caps, + type_definition=type_definition_caps, + implementation=implementation_caps, + color_provider=color_provider_caps, + folding_range=folding_range_caps, + declaration=declaration_caps, + selection_range=selection_range_caps, + call_hierarchy=call_hierarchy_caps, + semantic_tokens=semantic_tokens_caps, + linked_editing_range=linked_editing_range_caps, + ) + + message_action_item_caps = lsp.ShowMessageRequestClientCapabilitiesMessageActionItemType( + additional_properties_support=True + ) + show_message_caps = lsp.ShowMessageRequestClientCapabilities( + message_action_item=message_action_item_caps, + ) + show_document_caps = lsp.ShowDocumentClientCapabilities( + support=True, + ) + window_caps = lsp.WindowClientCapabilities( + show_message=show_message_caps, + show_document=show_document_caps, + work_done_progress=True, + ) + + regular_expressions_caps = lsp.RegularExpressionsClientCapabilities( + engine='ECMAScript', + version='ES2020', + ) + markdown_caps = lsp.MarkdownClientCapabilities( + parser='marked', + version='1.1.0' + ) + general_caps = lsp.GeneralClientCapabilities( + regular_expressions=regular_expressions_caps, + markdown=markdown_caps, + ) + + return lsp.ClientCapabilities( + workspace=workspace_caps, + text_document=text_document_caps, + window=window_caps, + general=general_caps, + ) + + client_capabilities = prepare_client_capabilities() + + client_info = lsp.InitializeParamsClientInfoType( + name=CLIENT_NAME, + version=CLIENT_VERSION, + ) + + root_path = os.path.realpath(WORKSPACE_FOLDER) + root_uri = from_fs_path(root_path) + assert root_uri is not None + workspace_folders = [ + lsp.WorkspaceFolder( + uri=root_uri, + name=os.path.basename(root_path) + ) + ] + + initialize_params = lsp.InitializeParams( + client_info=client_info, + locale='en', + root_path=root_path, + root_uri=root_uri, + capabilities=client_capabilities, + workspace_folders=workspace_folders, + ) + initialize_result = await asyncio.wait_for( + pyangtlc.initialize_async( + params=initialize_params, + ), + timeout=2.0 + ) + _validate_initialize_result(initialize_result) + + did_change_configuration_params = lsp.DidChangeConfigurationParams( + settings=None, + ) + pyangtlc.workspace_did_change_configuration( + params=did_change_configuration_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + test_doc = 'test-a.yang' + test_doc_path = os.path.join(os.path.realpath(WORKSPACE_FOLDER), test_doc) + doc_uri = from_fs_path(test_doc_path) + assert doc_uri is not None + + pyangtlc.test_step += 1 + + with open(test_doc_path, mode='r') as test_file: + data = test_file.read() + data = data.replace('deviate add', 'deviate replace') + with open(test_doc_path, mode='w') as test_file: + test_file.write(data) + + # Allow file sync + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + did_change_watched_files_params = lsp.DidChangeWatchedFilesParams( + changes=[ + lsp.FileEvent( + uri=doc_uri, + type=lsp.FileChangeType.Changed + ) + ] + ) + pyangtlc.workspace_did_change_watched_files( + params=did_change_watched_files_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + pyangtlc.test_step += 1 + + with open(test_doc_path, mode='a') as test_file: + test_file.write('garbage') + + # Allow file sync + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + did_change_watched_files_params = lsp.DidChangeWatchedFilesParams( + changes=[ + lsp.FileEvent( + uri=doc_uri, + type=lsp.FileChangeType.Changed + ) + ] + ) + pyangtlc.workspace_did_change_watched_files( + params=did_change_watched_files_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + pyangtlc.test_step += 1 + + with open(test_doc_path, mode='r') as test_file: + data = test_file.read() + data = data.replace('garbage', '') + data = data.replace('deviate replace', 'deviate add') + with open(test_doc_path, mode='w') as test_file: + test_file.write(data) + + did_change_watched_files_params = lsp.DidChangeWatchedFilesParams( + changes=[ + lsp.FileEvent( + uri=doc_uri, + type=lsp.FileChangeType.Changed + ) + ] + ) + pyangtlc.workspace_did_change_watched_files( + params=did_change_watched_files_params + ) + + # Allow textDocument/publishDiagnostics to arrive and be validated in sequence + # TODO: Should be done in a deterministic way + await asyncio.sleep(0.5) + + await asyncio.wait_for( + pyangtlc.shutdown_async( + params=None + ), + timeout=1.0 + ) + + pyangtlc.exit( + params=None + ) + +if __name__ == "__main__": + logging.basicConfig( + filename='pyangtlc.log', + filemode='w', + format='%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s', + datefmt='%H:%M:%S', + level=logging.DEBUG, + ) + + logger.info("------------------") + logger.info("Generic Test Suite") + logger.info("------------------") + asyncio.run(test_generic()) + + logger.info("----------------") + logger.info("Eglot Test Suite") + logger.info("----------------") + asyncio.run(test_eglot()) + + logger.info("-----------------") + logger.info("VSCode Test Suite") + logger.info("-----------------") + asyncio.run(test_vscode()) diff --git a/test/test_lsp/edit/1/test-a.yang b/test/test_lsp/edit/1/test-a.yang new file mode 100644 index 000000000..4092e98a0 --- /dev/null +++ b/test/test_lsp/edit/1/test-a.yang @@ -0,0 +1,119 @@ +module test-a { + yang-version 1.1; + namespace "urn:test-a"; + prefix testa; + + import ietf-netconf-acm { + prefix nacm; + } + import ietf-netconf { + prefix nc; + } + + organization + "Test Organization"; + contact + "Test Contact"; + description + "Module Test A"; + + revision 2024-03-09 { + description + "Revision Description"; + reference + "Revision Reference"; + } + + grouping test-grouping { + description + "Test Grouping"; + leaf test-grouping-leaf { + type string; + description + "Test Grouping Leaf"; + } + } + + rpc test-apple { + description + "Test Apple"; + input { + leaf leaf-name { + type string; + description + "Apple input leaf name"; + } + uses test-grouping; + } + } + + notification test-aleph { + description + "Test Aleph"; + uses test-grouping; + } + + notification test-aleph { + status deprecated; + description + "Test Aleph"; + uses test-grouping; + } + + deviation "/nacm:nacm" { + description + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + deviate add { + config false; + } + } + + container test-alpha { + presence "Test Alpha Presence"; + description + "Test Alpha"; + uses test-grouping; + leaf test-leaf { + type string; + description + "Test Leaf Description"; + } + choice test-choice { + description + "Test Choice"; + case test-case-a { + description + "Test Case A"; + leaf test-case-a-leaf { + type leafref { + path "/testa:test-alpha/testa:test-leaf"; + } + description + "Test Case A Leaf"; + } + } + case test-case-b { + description + "Test Case B"; + leaf test-case-b-leaf { + type string { + pattern '[ ]+'; + } + description + "Test Case B Leaf"; + } + } + case test-case-c { + description + "Test Case B"; + leaf test-case-c-leaf { + type leafref { + path "/testa:test-alpha/testa:test-grouping-leaf"; + } + description + "Test Case C Leaf"; + } + } + } + } +} diff --git a/test/test_lsp/expect/0/test-a.json b/test/test_lsp/expect/0/test-a.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/0/test-a.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/1/test-a.json b/test/test_lsp/expect/1/test-a.json new file mode 100644 index 000000000..044db4a88 --- /dev/null +++ b/test/test_lsp/expect/1/test-a.json @@ -0,0 +1,78 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 66, + "character": 0 + }, + "end": { + "line": 67, + "character": 0 + } + }, + "message": "the \"config\" property already exists in node \"ietf-netconf-acm::nacm\"", + "severity": 1, + "code": "BAD_DEVIATE_ADD", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 55, + "character": 0 + }, + "end": { + "line": 56, + "character": 0 + } + }, + "message": "there is already a child node to \"test-a\" at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:1 with the name \"test-aleph\" defined at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:50", + "severity": 1, + "code": "DUPLICATE_CHILD_NAME", + "source": "pyang", + "tags": [], + "relatedInformation": [ + { + "location": { + "uri": "file:///home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang", + "range": { + "start": { + "line": 49, + "character": 0 + }, + "end": { + "line": 50, + "character": 0 + } + } + }, + "message": "Original Child" + } + ] + }, + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 9, + "character": 0 + } + }, + "message": "imported module \"ietf-netconf\" not used", + "severity": 3, + "code": "UNUSED_IMPORT", + "source": "pyang", + "tags": [ + 1 + ], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/1/test-b.json b/test/test_lsp/expect/1/test-b.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/1/test-b.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/1/test-c.json b/test/test_lsp/expect/1/test-c.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/1/test-c.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/1/test-d.json b/test/test_lsp/expect/1/test-d.json new file mode 100644 index 000000000..ebfa291eb --- /dev/null +++ b/test/test_lsp/expect/1/test-d.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"namespace\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"prefix\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/2/test-a.json b/test/test_lsp/expect/2/test-a.json new file mode 100644 index 000000000..6735521e2 --- /dev/null +++ b/test/test_lsp/expect/2/test-a.json @@ -0,0 +1,60 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 55, + "character": 0 + }, + "end": { + "line": 56, + "character": 0 + } + }, + "message": "there is already a child node to \"test-a\" at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:1 with the name \"test-aleph\" defined at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:50", + "severity": 1, + "code": "DUPLICATE_CHILD_NAME", + "source": "pyang", + "tags": [], + "relatedInformation": [ + { + "location": { + "uri": "file:///home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang", + "range": { + "start": { + "line": 49, + "character": 0 + }, + "end": { + "line": 50, + "character": 0 + } + } + }, + "message": "Original Child" + } + ] + }, + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 9, + "character": 0 + } + }, + "message": "imported module \"ietf-netconf\" not used", + "severity": 3, + "code": "UNUSED_IMPORT", + "source": "pyang", + "tags": [ + 1 + ], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/2/test-b.json b/test/test_lsp/expect/2/test-b.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/2/test-b.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/2/test-c.json b/test/test_lsp/expect/2/test-c.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/2/test-c.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/2/test-d.json b/test/test_lsp/expect/2/test-d.json new file mode 100644 index 000000000..ebfa291eb --- /dev/null +++ b/test/test_lsp/expect/2/test-d.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"namespace\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"prefix\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/3/test-a.json b/test/test_lsp/expect/3/test-a.json new file mode 100644 index 000000000..f656d5e16 --- /dev/null +++ b/test/test_lsp/expect/3/test-a.json @@ -0,0 +1,23 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 119, + "character": 0 + }, + "end": { + "line": 120, + "character": 0 + } + }, + "message": "trailing garbage after module", + "severity": 1, + "code": "TRAILING_GARBAGE", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/3/test-b.json b/test/test_lsp/expect/3/test-b.json new file mode 100644 index 000000000..a80ab32d7 --- /dev/null +++ b/test/test_lsp/expect/3/test-b.json @@ -0,0 +1,23 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 24, + "character": 0 + }, + "end": { + "line": 25, + "character": 0 + } + }, + "message": "the key \"test-grouping-leaf\" does not reference an existing leaf", + "severity": 1, + "code": "BAD_KEY", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/3/test-c.json b/test/test_lsp/expect/3/test-c.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/3/test-c.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/3/test-d.json b/test/test_lsp/expect/3/test-d.json new file mode 100644 index 000000000..ebfa291eb --- /dev/null +++ b/test/test_lsp/expect/3/test-d.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"namespace\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"prefix\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/4/test-a.json b/test/test_lsp/expect/4/test-a.json new file mode 100644 index 000000000..044db4a88 --- /dev/null +++ b/test/test_lsp/expect/4/test-a.json @@ -0,0 +1,78 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 66, + "character": 0 + }, + "end": { + "line": 67, + "character": 0 + } + }, + "message": "the \"config\" property already exists in node \"ietf-netconf-acm::nacm\"", + "severity": 1, + "code": "BAD_DEVIATE_ADD", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 55, + "character": 0 + }, + "end": { + "line": 56, + "character": 0 + } + }, + "message": "there is already a child node to \"test-a\" at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:1 with the name \"test-aleph\" defined at /home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang:50", + "severity": 1, + "code": "DUPLICATE_CHILD_NAME", + "source": "pyang", + "tags": [], + "relatedInformation": [ + { + "location": { + "uri": "file:///home/esmasth/code/pyang/test/test_lsp/workspace/test-a.yang", + "range": { + "start": { + "line": 49, + "character": 0 + }, + "end": { + "line": 50, + "character": 0 + } + } + }, + "message": "Original Child" + } + ] + }, + { + "range": { + "start": { + "line": 8, + "character": 0 + }, + "end": { + "line": 9, + "character": 0 + } + }, + "message": "imported module \"ietf-netconf\" not used", + "severity": 3, + "code": "UNUSED_IMPORT", + "source": "pyang", + "tags": [ + 1 + ], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/4/test-b.json b/test/test_lsp/expect/4/test-b.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/4/test-b.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/4/test-c.json b/test/test_lsp/expect/4/test-c.json new file mode 100644 index 000000000..0cf1cd60b --- /dev/null +++ b/test/test_lsp/expect/4/test-c.json @@ -0,0 +1,4 @@ +{ + "items": [], + "kind": "full" +} diff --git a/test/test_lsp/expect/4/test-d.json b/test/test_lsp/expect/4/test-d.json new file mode 100644 index 000000000..ebfa291eb --- /dev/null +++ b/test/test_lsp/expect/4/test-d.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"namespace\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 1, + "character": 0 + } + }, + "message": "expected keyword \"prefix\" as child to \"module\"", + "severity": 1, + "code": "EXPECTED_KEYWORD_2", + "source": "pyang", + "tags": [], + "relatedInformation": [] + } + ], + "kind": "full" +} diff --git a/test/test_lsp/expect/test-a.yang b/test/test_lsp/expect/test-a.yang new file mode 100644 index 000000000..0788c9cf8 --- /dev/null +++ b/test/test_lsp/expect/test-a.yang @@ -0,0 +1,119 @@ +module test-a { + yang-version 1.1; + namespace "urn:test-a"; + prefix testa; + + import ietf-netconf-acm { + prefix nacm; + } + import ietf-netconf { + prefix nc; + } + + organization + "Test Organization"; + contact + "Test Contact"; + description + "Module Test A"; + + revision 2024-03-09 { + description + "Revision Description"; + reference + "Revision Reference"; + } + + grouping test-grouping { + description + "Test Grouping"; + leaf test-grouping-leaf { + type string; + description + "Test Grouping Leaf"; + } + } + + rpc test-apple-de-Finibus-Bonorum-et-Malorum-de-Finibus-Bonorum-et-Malorum { + description + "Test Apple"; + input { + leaf leaf-name { + type string; + description + "Apple input leaf name"; + } + uses test-grouping; + } + } + + notification test-aleph { + description + "Test Aleph"; + uses test-grouping; + } + + notification test-aleph { + status deprecated; + description + "Test Aleph"; + uses test-grouping; + } + + deviation "/nacm:nacm" { + description + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + deviate add { + config false; + } + } + + container test-alpha { + presence "Test Alpha Presence"; + description + "Test Alpha"; + uses test-grouping; + leaf test-leaf { + type string; + description + "Test Leaf Description"; + } + choice test-choice { + description + "Test Choice"; + case test-case-a { + description + "Test Case A"; + leaf test-case-a-leaf { + type leafref { + path "/testa:test-alpha/testa:test-leaf"; + } + description + "Test Case A Leaf"; + } + } + case test-case-b { + description + "Test Case B"; + leaf test-case-b-leaf { + type string { + pattern '[ ]+'; + } + description + "Test Case B Leaf"; + } + } + case test-case-c { + description + "Test Case B"; + leaf test-case-c-leaf { + type leafref { + path "/testa:test-alpha/testa:test-grouping-leaf"; + } + description + "Test Case C Leaf"; + } + } + } + } +} diff --git a/test/test_lsp/initialize-eglot-pyangtlc.json b/test/test_lsp/initialize-eglot-pyangtlc.json new file mode 100644 index 000000000..66e56e78a --- /dev/null +++ b/test/test_lsp/initialize-eglot-pyangtlc.json @@ -0,0 +1,197 @@ +{ + "processId": null, + "clientInfo": { + "name": "pyangtest", + "version": "v0.1" + }, + "rootPath": "", + "rootUri": "", + "capabilities": { + "workspace": { + "applyEdit": true, + "executeCommand": { + "dynamicRegistration": false + }, + "workspaceEdit": { + "documentChanges": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true + }, + "symbol": { + "dynamicRegistration": false + }, + "configuration": true, + "workspaceFolders": true + }, + "textDocument": { + "synchronization": { + "dynamicRegistration": false, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": false, + "completionItem": { + "snippetSupport": true, + "deprecatedSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "details", + "additionalTextEdits" + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + } + }, + "contextSupport": true + }, + "hover": { + "dynamicRegistration": false, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": false, + "signatureInformation": { + "parameterInformation": { + "labelOffsetSupport": true + }, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "activeParameterSupport": true + } + }, + "references": { + "dynamicRegistration": false + }, + "definition": { + "dynamicRegistration": false, + "linkSupport": true + }, + "declaration": { + "dynamicRegistration": false, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": false, + "linkSupport": true + }, + "typeDefinition": { + "dynamicRegistration": false, + "linkSupport": true + }, + "documentSymbol": { + "dynamicRegistration": false, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + } + }, + "documentHighlight": { + "dynamicRegistration": false + }, + "codeAction": { + "dynamicRegistration": false, + "resolveSupport": { + "properties": [ + "edit", + "command" + ] + }, + "dataSupport": true, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "isPreferredSupport": true + }, + "formatting": { + "dynamicRegistration": false + }, + "rangeFormatting": { + "dynamicRegistration": false + }, + "rename": { + "dynamicRegistration": false + }, + "inlayHint": { + "dynamicRegistration": false + }, + "publishDiagnostics": { + "relatedInformation": false, + "codeDescriptionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + } + } + }, + "window": { + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "positionEncodings": [ + "utf-32", + "utf-8", + "utf-16" + ] + } + }, + "workspaceFolders": [ + { + "uri": "", + "name": "" + } + ] +} diff --git a/test/test_lsp/initialize-eglot.json b/test/test_lsp/initialize-eglot.json new file mode 100644 index 000000000..304fd8619 --- /dev/null +++ b/test/test_lsp/initialize-eglot.json @@ -0,0 +1,199 @@ +{ + "processId": null, + "clientInfo": { + "name": "Eglot", + "version": "1.17" + }, + "rootPath": "/home/esmasth/code/test-yang/", + "rootUri": "file:///home/esmasth/code/test-yang", + "initializationOptions": {}, + "capabilities": { + "workspace": { + "applyEdit": true, + "executeCommand": { + "dynamicRegistration": false + }, + "workspaceEdit": { + "documentChanges": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true + }, + "symbol": { + "dynamicRegistration": false + }, + "configuration": true, + "workspaceFolders": true + }, + "textDocument": { + "synchronization": { + "dynamicRegistration": false, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": false, + "completionItem": { + "snippetSupport": true, + "deprecatedSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "details", + "additionalTextEdits" + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + } + }, + "contextSupport": true + }, + "hover": { + "dynamicRegistration": false, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": false, + "signatureInformation": { + "parameterInformation": { + "labelOffsetSupport": true + }, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "activeParameterSupport": true + } + }, + "references": { + "dynamicRegistration": false + }, + "definition": { + "dynamicRegistration": false, + "linkSupport": true + }, + "declaration": { + "dynamicRegistration": false, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": false, + "linkSupport": true + }, + "typeDefinition": { + "dynamicRegistration": false, + "linkSupport": true + }, + "documentSymbol": { + "dynamicRegistration": false, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + } + }, + "documentHighlight": { + "dynamicRegistration": false + }, + "codeAction": { + "dynamicRegistration": false, + "resolveSupport": { + "properties": [ + "edit", + "command" + ] + }, + "dataSupport": true, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "isPreferredSupport": true + }, + "formatting": { + "dynamicRegistration": false + }, + "rangeFormatting": { + "dynamicRegistration": false + }, + "rename": { + "dynamicRegistration": false + }, + "inlayHint": { + "dynamicRegistration": false + }, + "publishDiagnostics": { + "relatedInformation": false, + "codeDescriptionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + } + } + }, + "window": { + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "positionEncodings": [ + "utf-32", + "utf-8", + "utf-16" + ] + }, + "experimental": {} + }, + "workspaceFolders": [ + { + "uri": "file:///home/esmasth/code/test-yang", + "name": "~/code/test-yang/" + } + ] +} diff --git a/test/test_lsp/initialize-vscode-pyangtlc.json b/test/test_lsp/initialize-vscode-pyangtlc.json new file mode 100644 index 000000000..26fd8a8a0 --- /dev/null +++ b/test/test_lsp/initialize-vscode-pyangtlc.json @@ -0,0 +1,399 @@ +{ + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + } + }, + "executeCommand": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "configuration": true, + "semanticTokens": { + "refreshSupport": true + }, + "codeLens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "willCreate": true, + "didRename": true, + "willRename": true, + "didDelete": true, + "willDelete": true + } + }, + "textDocument": { + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + } + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "contextSupport": true + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "publishDiagnostics": { + "relatedInformation": true, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "versionSupport": false, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "dynamicRegistration": true, + "overlappingTokenSupport": false, + "multilineTokenSupport": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + } + }, + "window": { + "workDoneProgress": true, + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + } + }, + "general": { + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + } + } + }, + "processId": null, + "rootPath": "/home/esmasth/code/pyang/test/test_lsp/workspace", + "rootUri": "file:///home/esmasth/code/pyang/test/test_lsp/workspace", + "workspaceFolders": [ + { + "uri": "file:///home/esmasth/code/pyang/test/test_lsp/workspace", + "name": "workspace" + } + ], + "clientInfo": { + "name": "pyangtlc", + "version": "v0.1" + }, + "locale": "en" +} diff --git a/test/test_lsp/initialize-vscode.json b/test/test_lsp/initialize-vscode.json new file mode 100644 index 000000000..15fa4775a --- /dev/null +++ b/test/test_lsp/initialize-vscode.json @@ -0,0 +1,399 @@ +{ + "processId": 30738, + "clientInfo": { + "name": "Visual Studio Code", + "version": "1.88.1" + }, + "locale": "en", + "rootPath": "/home/esmasth/code/test-yang", + "rootUri": "file:///home/esmasth/code/test-yang", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "configuration": true, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + } + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + } + } + }, + "workspaceFolders": [ + { + "uri": "file:///home/esmasth/code/test-yang", + "name": "test-yang" + } + ] +} diff --git a/test/test_lsp/pyangtlc.puml b/test/test_lsp/pyangtlc.puml new file mode 100644 index 000000000..8c1fa6bd5 --- /dev/null +++ b/test/test_lsp/pyangtlc.puml @@ -0,0 +1,92 @@ +@startuml "pyang Test LSP Client" + +title "Basic Validation" + +participant tlc as "pyang\nTest LSP Client" +participant ls as "pyang\nLSP Server" + +--> tlc : Start Test LSP Client +activate tlc + +note left of tlc + Build workspace with + all *.yang files. +end note + +tlc --> ls : Start STDIO mode server\n**pyang --lsp** +activate ls + +note right of ls + Prepare LSP server + capabilities and defaults +end note +note left of tlc + Prepare LSP client + capabilities as Eglot +end note + +tlc [#Blue]-> ls : **initialize** request +note right of ls + Build LSP server + workspace +end note +tlc <-[#Blue] ls : **initialize** response +note left of tlc + Validate response for + Server capabilities +end note + +tlc [#Purple]-> ls : **initialized** notification +' note right of ls +' Build and validate workspace +' with all *.yang files +' end note +' alt #Salmon "Client has //textDocument/publishDiagnostics// capability" +' loop "For each *.yang in workspace" +' tlc <-[#Purple] ls : **textDocument/publishDiagnostics** notification +' note left of tlc +' Validate response for +' Server capabilities +' end note +' end +' end + +opt + tlc -> ls : **textDocument/didOpen** notification +end + +tlc [#Purple]-> ls : **workspace/didChangeConfiguration** notification +note right of ls + Build and validate workspace + with all *.yang files +end note +' alt #Salmon "Configuration different from defaults" +' note right of ls +' Rebuild and revalidate workspace +' with all *.yang files +' end note + alt #White "Client has //textDocument/publishDiagnostics// capability" + loop "For each *.yang in workspace" + tlc <-[#Purple] ls : **textDocument/publishDiagnostics** notification + end + end +' end + +... + +tlc [#Blue]-> ls : **shutdown** request +tlc <-[#Blue] ls : **shutdown** response +note left of tlc + TODO: Validate response + error code, if feasible +end note + +tlc [#Purple]-> ls : **exit** notification +tlc <-- ls : **pyang --lsp** exit code +deactivate ls +note left of tlc + TODO: Validate exit + code, if feasible +end note + +@enduml diff --git a/test/test_lsp/workspace/subdir/test-d.yang b/test/test_lsp/workspace/subdir/test-d.yang new file mode 100644 index 000000000..324618917 --- /dev/null +++ b/test/test_lsp/workspace/subdir/test-d.yang @@ -0,0 +1,3 @@ +module test-d { + +} diff --git a/test/test_lsp/workspace/test-a.yang b/test/test_lsp/workspace/test-a.yang new file mode 100644 index 000000000..9383511b2 --- /dev/null +++ b/test/test_lsp/workspace/test-a.yang @@ -0,0 +1,119 @@ +module test-a { + yang-version 1.1; + namespace "urn:test-a"; + prefix testa; + + import ietf-netconf-acm { + prefix nacm; + } + import ietf-netconf { + prefix nc; + } + + organization + "Test Organization"; + contact + "Test Contact"; + description + "Module Test A"; + + revision 2024-03-09 { + description + "Revision Description"; + reference + "Revision Reference"; + } + + grouping test-grouping { + description + "Test Grouping"; + leaf test-grouping-leaf { + type string; + description + "Test Grouping Leaf"; + } + } + + rpc test-apple-de-Finibus-Bonorum-et-Malorum-de-Finibus-Bonorum-et-Malorum { + description + "Test Apple"; + input { + leaf leaf-name { + type string; + description + "Apple input leaf name"; + } + uses test-grouping; + } + } + + notification test-aleph { + description + "Test Aleph"; + uses test-grouping; + } + + notification test-aleph { + status deprecated; + description + "Test Aleph"; + uses test-grouping; + } + + deviation "/nacm:nacm" { + description + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + deviate add { + config false; + } + } + + container test-alpha { + presence "Test Alpha Presence"; + description + "Test Alpha"; + uses test-grouping; + leaf test-leaf { + type string; + description + "Test Leaf Description"; + } + choice test-choice { + description + "Test Choice"; + case test-case-a { + description + "Test Case A"; + leaf test-case-a-leaf { + type leafref { + path "/testa:test-alpha/testa:test-leaf"; + } + description + "Test Case A Leaf"; + } + } + case test-case-b { + description + "Test Case B"; + leaf test-case-b-leaf { + type string { + pattern '[ ]+'; + } + description + "Test Case B Leaf"; + } + } + case test-case-c { + description + "Test Case B"; + leaf test-case-c-leaf { + type leafref { + path "/testa:test-alpha/testa:test-grouping-leaf"; + } + description + "Test Case C Leaf"; + } + } + } + } +} diff --git a/test/test_lsp/workspace/test-b.yang b/test/test_lsp/workspace/test-b.yang new file mode 100644 index 000000000..af5469f75 --- /dev/null +++ b/test/test_lsp/workspace/test-b.yang @@ -0,0 +1,37 @@ +module test-b { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:test-b"; + prefix testb; + + import test-a { + prefix testa; + } + + description + "Module Test B"; + + augment "/testa:test-alpha" { + description + "Augmenting Test Alpha"; + } + + container test-container { + uses testa:test-grouping; + } + container test-container2 { + uses testa:test-grouping; + } + list test-list { + key "test-grouping-leaf"; + description + "Test List"; + leaf test-grouping-leaf2 { + type string; + } + uses testa:test-grouping; + container test-container3 { + description + "Test Container 3"; + } + } +} diff --git a/test/test_lsp/workspace/test-c.yang b/test/test_lsp/workspace/test-c.yang new file mode 100644 index 000000000..2de4bfeb2 --- /dev/null +++ b/test/test_lsp/workspace/test-c.yang @@ -0,0 +1,5 @@ +module test-c { + yang-version 1.1; + namespace "urn:test-c"; + prefix testc; +}