├── .editorconfig ├── .flake8 ├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yml ├── README.md ├── docs ├── Makefile ├── _templates │ └── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ └── module.rst ├── conf.py ├── index.rst ├── make.bat ├── plc_code │ ├── .gitignore │ └── TwinCAT Project │ │ ├── MyPlcProject │ │ ├── DUTs │ │ │ └── ST_ExampleStruct.TcDUT │ │ ├── GVLs │ │ │ └── GVL_Main.TcGVL │ │ ├── MyPlcProject.plcproj │ │ ├── POUs │ │ │ ├── ExampleFolder │ │ │ │ ├── FB_ExampleFolder_FunctionBlock.TcPOU │ │ │ │ ├── F_ExampleFolder_Function.TcPOU │ │ │ │ └── ST_ExampleFolder_Struct.TcDUT │ │ │ ├── FB_ExampleFunctionBlock.TcPOU │ │ │ ├── F_ExampleFunction.TcPOU │ │ │ └── MAIN.TcPOU │ │ └── PlcTask.TcTTO │ │ ├── TwinCAT Project.sln │ │ └── TwinCAT Project.tsproj └── src │ ├── config.rst │ ├── directives.rst │ ├── examples.rst │ ├── limitations.rst │ ├── modules.rst │ └── quickstart.rst ├── examples └── docs │ ├── Makefile │ ├── conf.py │ ├── index.rst │ ├── make.bat │ ├── other_languages.rst │ ├── plc.rst │ └── plc_auto.rst ├── pyproject.toml ├── src └── plcdoc │ ├── __init__.py │ ├── __version__.py │ ├── auto_directives.py │ ├── common.py │ ├── directives.py │ ├── documenters.py │ ├── domain.py │ ├── extension.py │ ├── interpreter.py │ ├── roles.py │ └── st_declaration.tx └── tests ├── __init__.py ├── conftest.py ├── plc_code ├── E_Filter.txt ├── E_Options.txt ├── FB_Comments.txt ├── FB_MyBlock.txt ├── FB_MyBlockExtended.txt ├── FB_Variables.txt ├── GlobalVariableList.txt ├── Main.txt ├── MyStructure.txt ├── MyStructureExtended.txt ├── Properties.txt ├── RegularFunction.txt ├── T_ALIAS.txt ├── TwinCAT PLC │ ├── .gitignore │ ├── MyPLC │ │ ├── DUTs │ │ │ ├── E_Options.TcDUT │ │ │ ├── MyStructure.TcDUT │ │ │ └── MyStructureExtended.TcDUT │ │ ├── GVLs │ │ │ └── GVL_Main.TcGVL │ │ ├── MyPLC.plcproj │ │ ├── POUs │ │ │ ├── FB_MyBlock.TcPOU │ │ │ ├── FB_SecondBlock.TcPOU │ │ │ ├── MAIN.TcPOU │ │ │ ├── PlainFunction.TcPOU │ │ │ ├── PlainFunctionBlock.TcPOU │ │ │ └── RegularFunction.TcPOU │ │ └── PlcTask.TcTTO │ ├── TwinCAT Solution.sln │ └── TwinCAT Solution.tsproj └── Unions.txt ├── roots ├── test-domain-plc │ ├── conf.py │ └── index.rst ├── test-plc-autodoc │ ├── conf.py │ ├── index.rst │ └── src_plc │ │ ├── AutoFunction.TcPOU │ │ ├── AutoFunctionBlock.TcPOU │ │ ├── AutoGVL.TcGVL │ │ ├── AutoStruct.TcDUT │ │ ├── FB_MyBlock.TcPOU │ │ ├── PlainFunction.TcPOU │ │ ├── PlainFunctionBlock.TcPOU │ │ └── RegularFunction.TcPOU ├── test-plc-project │ ├── conf.py │ ├── index.rst │ └── src_plc │ │ ├── DUTs │ │ ├── E_Error.TcDUT │ │ ├── ST_MyStruct.TcDUT │ │ └── T_ALIAS.TcDUT │ │ ├── MyPLC.plcproj │ │ └── POUs │ │ ├── FB_MyBlock.TcPOU │ │ ├── FB_SecondBlock.TcPOU │ │ ├── F_SyntaxError.TcPOU │ │ ├── MAIN.TcPOU │ │ ├── PlainFunction.TcPOU │ │ ├── PlainFunctionBlock.TcPOU │ │ └── RegularFunction.TcPOU └── test-plc-ref │ ├── conf.py │ └── index.rst ├── test_domain_plc.py ├── test_interpreter.py ├── test_plc_autodoc.py ├── test_plc_project.py ├── test_plc_ref.py └── test_st_grammar.py /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.rst] 2 | indent_size = 3 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Match with black (using flake8-bugbear) 3 | max-line-length = 80 4 | extend-select = B950 5 | extend-ignore = E203,E501,E701 6 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Test the source for Black reformatting (pass/fail) 3 | # 4 | 5 | name: Linting 6 | 7 | on: push 8 | 9 | jobs: 10 | 11 | flake8-black-lint: 12 | runs-on: ubuntu-latest 13 | name: Flake8 / Black code check 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.11" 19 | - uses: py-actions/flake8@v2 20 | with: 21 | path: "./src" 22 | plugins: "flake8-bugbear flake8-black" 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This workflow will install the package and run the unittests. 3 | # 4 | 5 | name: PyTest tests 6 | 7 | on: push 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | name: Pytest ${{ matrix.python-version }}, ${{matrix.os}} 16 | 17 | runs-on: ${{matrix.os}} 18 | 19 | strategy: 20 | fail-fast: true 21 | matrix: 22 | os: ["ubuntu-latest"] 23 | python-version: ["3.9", "3.10", "3.11", "3.12"] 24 | include: 25 | - os: "windows-latest" 26 | python-version: "3.11" 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | submodules: true 32 | - name: Set up Python ${{matrix.python-version}} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{matrix.python-version}} 36 | cache: "pip" 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install -e .[test] 41 | - name: Test with pytest 42 | run: | 43 | pytest --cov=src/ --cov-report=term 44 | - name: Upload coverage reports to Codecov 45 | if: ${{matrix.python-version}} == '3.10' && ${{matrix.os}} == 'ubuntu-latest' 46 | uses: codecov/codecov-action@v3 47 | env: 48 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vscode/ 4 | 5 | # Python stuff 6 | venv/ 7 | .venv/ 8 | venv*/ 9 | __pycache__/ 10 | *.egg-info/ 11 | .pytest_cache/ 12 | dist/ 13 | 14 | build/ 15 | _build/ 16 | _autosummary/ 17 | 18 | # Model display files 19 | *.dot 20 | 21 | .coverage 22 | htmlcov/ 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/plc_code/extern/lcls-twincat-general"] 2 | path = tests/plc_code/extern/lcls-twincat-general 3 | url = https://github.com/pcdshub/lcls-twincat-general.git 4 | [submodule "tests/plc_code/extern/lcls-twincat-motion"] 5 | path = tests/plc_code/extern/lcls-twincat-motion 6 | url = https://github.com/pcdshub/lcls-twincat-motion.git 7 | [submodule "tests/plc_code/extern/TcUnit"] 8 | path = tests/plc_code/extern/TcUnit 9 | url = https://github.com/tcunit/TcUnit.git 10 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the "docs/" directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | # formats: 20 | # - pdf 21 | # - epub 22 | 23 | # Optional but recommended, declare the Python requirements required 24 | # to build your documentation 25 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 26 | python: 27 | install: 28 | - method: pip 29 | path: . 30 | extra_requirements: 31 | - doc 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PLC Sphinx Parser 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/plc-doc/badge/?version=latest)](https://plc-doc.readthedocs.io/latest/?badge=latest) 4 | [![Unit tests](https://github.com/DEMCON/plcdoc/actions/workflows/tests.yml/badge.svg)](https://github.com/DEMCON/plcdoc/actions) 5 | [![codecov](https://codecov.io/gh/DEMCON/plcdoc/graph/badge.svg?token=xMg0U6mX2r)](https://codecov.io/gh/DEMCON/plcdoc) 6 | 7 | This is a work-in-progress of a tool to get documentation with Sphinx from TwinCAT PLC. 8 | The focus is on PLC code made with Structured Text (ST), i.e. the IEC 61131-3 standard. 9 | 10 | At the basis is the [TextX](https://github.com/textX/textX) package to parse some of the PLC code. 11 | 12 | This package allows for recognition of ST code such that definitions can be manually typed in. 13 | More useful however is the feature to automatically parse declarations from ST and insert the results in your Sphinx 14 | document. 15 | 16 | ## Why 17 | 18 | TwinCAT has a built-in documentation system. However, it is only semi-official and it cannot be exported to stand-alone 19 | documents. Currently it is common to write your program documentation externally and attach it with your source. The 20 | downsides of this approach are clear: structural or name changes must all be done twice, which can be easily forgotten. 21 | 22 | It would be ideal to, like with many modern languages, produce from-source documentation on the fly with continuous 23 | integration. This way you can focus exclusively on your code while up-to-date docs are always available. 24 | 25 | ## How to use 26 | 27 | **Warning:** `plcdoc` is still in development and although some documentation can be made, you will likely run into many warnings and errors. 28 | 29 | ### Install 30 | 31 | Install from [pypi.org](https://pypi.org/project/plcdoc/) with: 32 | ``` 33 | pip install plcdoc 34 | ``` 35 | 36 | ### Configuration 37 | 38 | In your `conf.py`, add the extension: 39 | 40 | ```python 41 | extensions = ["plcdoc"] 42 | ``` 43 | 44 | For automatic source parsing, either list your PLC source files: 45 | 46 | ```python 47 | plc_sources = [ 48 | "path/to/file1.TcPOU", 49 | # ... 50 | ] 51 | ``` 52 | 53 | And/or list a project file directly: 54 | 55 | ```python 56 | plc_project = "path/to/project.plcproj" 57 | ``` 58 | 59 | When using a project file it will be scoured for listed sources and those sources will be included directly. 60 | 61 | ### Manual typing 62 | 63 | You define Structured Text objects like so: 64 | 65 | ```rst 66 | .. plc:function:: MyFunction(myFloat, myInt) : BOOL 67 | 68 | :param REAL myFloat: First variable 69 | :param UDINT myInt: Second variable, very important 70 | 71 | This is the general description. 72 | ``` 73 | 74 | ### Auto typing 75 | 76 | You can insert in-source definitions like this: 77 | 78 | ```rst 79 | .. plc:autofunction MyFunction 80 | ``` 81 | 82 | ### Structured Text doc comments 83 | 84 | Follow the [Beckhoff comment style](https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/6158078987.html&id=) 85 | to allow parsing from source. Fortunately, this is close to e.g. the Python style of doc comments. 86 | 87 | For example, the declaration to make the `plc:autofunction` above give the same result as the `plc:function` could be: 88 | 89 | ``` 90 | (* 91 | This is the general description. 92 | *) 93 | FUNCTION MyFunction : BOOL 94 | VAR_INPUT 95 | myFloat : REAL; // First variable 96 | myInt : UDINT; // Second variable, very important 97 | VAR_END 98 | ``` 99 | 100 | Types, arguments, etc. are deduced from the source code itself and corresponding comments are put alongside. 101 | 102 | ## Possibilities and limitations 103 | 104 | The goal is that all code inside `` tags can be parsed. Everything outside, e.g. inside 105 | `` tags, cannot and will be ignored. 106 | 107 | Although all common notations should be supported, due to the flexibility of Structured Text (and any programming 108 | languages in general), there will be cases where source parsing fails. It is recommended to set up docs generation 109 | early on to identify failures quickly. 110 | 111 | The biggest source of errors inside declarations is initial expressions. For example, a variable could be initialized 112 | as a sum, involving other variables, and include various literal notations. This is hard to capture in full. 113 | 114 | ## Developing 115 | 116 | ### Setup 117 | 118 | Get the development tools and make your installation editable by running: 119 | ``` 120 | pip install -e .[test,doc] 121 | ``` 122 | 123 | ### Tests 124 | 125 | Run all tests with `python -m pytest`. 126 | 127 | The structure is based on the Sphinx unittest principle. There are actual doc roots present which are processed as 128 | normal during a test. 129 | 130 | Run with coverage with `python -m pytest --cov`. Add `--cov-report=html` to produce an HTML test report. 131 | 132 | ### Basis and inspirations 133 | 134 | There are a bunch of projects linking new languages to Sphinx: 135 | 136 | * C++: https://github.com/breathe-doc/breathe (language, typing and auto-doc) 137 | * MATLAB: https://github.com/sphinx-contrib/matlabdomain (language, typing and auto-doc) 138 | * JavaScript: https://github.com/mozilla/sphinx-js (language, typing and auto-doc) 139 | * Python (`autodoc`): https://github.com/sphinx-doc/sphinx/tree/master/sphinx/ext/autodoc (auto-doc) 140 | 141 | `breathe` is the biggest and most complete, but very abstract. `sphinxcontrib-matlabdomain` was the main inspiration for 142 | the package layout. The auto-documentation however mimics the default Python `autodoc` extension very closely. 143 | 144 | ## References 145 | 146 | Other useful projects: 147 | 148 | * [TcTools](https://github.com/DEMCON/twincat-tools): 149 | * A package with helper tools for developing TwinCAT PLC code (also made by us). 150 | * [Blark](https://github.com/klauer/blark): 151 | * Parsing of PLC Structured Text code in Python using Lark. 152 | Very impressive and much more complete than the PLC parsing done here, but slight overkill for the purpose here. 153 | * [TcBlack](https://github.com/Roald87/TcBlack): 154 | * Black-inspired formatting tool for PLC Structured Text. 155 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. auto{{ objtype }}:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: {{ _('Module Attributes') }} 8 | 9 | .. autosummary:: 10 | :toctree: 11 | {% for item in attributes %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block functions %} 18 | {% if functions %} 19 | .. rubric:: {{ _('Functions') }} 20 | 21 | .. autosummary:: 22 | :toctree: 23 | {% for item in functions %} 24 | {{ item }} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block classes %} 30 | {% if classes %} 31 | .. rubric:: {{ _('Classes') }} 32 | 33 | .. autosummary:: 34 | :toctree: 35 | {% for item in classes %} 36 | {{ item }} 37 | {%- endfor %} 38 | {% endif %} 39 | {% endblock %} 40 | 41 | {% block exceptions %} 42 | {% if exceptions %} 43 | .. rubric:: {{ _('Exceptions') }} 44 | 45 | .. autosummary:: 46 | :toctree: 47 | {% for item in exceptions %} 48 | {{ item }} 49 | {%- endfor %} 50 | {% endif %} 51 | {% endblock %} 52 | 53 | {% block modules %} 54 | {% if modules %} 55 | .. rubric:: Modules 56 | 57 | .. autosummary:: 58 | :toctree: 59 | :recursive: 60 | {% for item in modules %} 61 | {{ item }} 62 | {%- endfor %} 63 | {% endif %} 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "PLC Doc" 10 | copyright = "2023, Robert Roos " 11 | author = "Robert Roos " 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = [ 17 | "plcdoc", 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.autosectionlabel", 20 | "sphinx.ext.autosummary", 21 | "sphinx_rtd_theme", 22 | ] 23 | 24 | templates_path = ["_templates"] 25 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 26 | 27 | # -- Options for autodoc -------------------------------------------- 28 | autoclass_content = "both" 29 | 30 | # -- Options for autosectionlabel -------------------------------------------- 31 | autosectionlabel_prefix_document = True 32 | 33 | # -- Options for autosummary -------------------------------------------- 34 | autosummary_generate = True 35 | 36 | # -- Options for HTML output ------------------------------------------------- 37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 38 | 39 | html_theme = "sphinx_rtd_theme" 40 | html_static_path = ["_static"] 41 | 42 | # -- Options for PLC-doc ------------------------------------------------- 43 | plc_project = "plc_code/TwinCAT Project/MyPlcProject/MyPlcProject.plcproj" 44 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. PLC Doc documentation master file, created by 2 | sphinx-quickstart on Thu Aug 10 13:15:09 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | .. 8 | Header format: 9 | 10 | # with overline, for chapters 11 | =, for sections 12 | -, for subsections 13 | ^, for subsubsections 14 | ", for paragraphs 15 | 16 | 17 | ######################## 18 | PLC Doc's documentation! 19 | ######################## 20 | 21 | ``plcdoc`` is a package to get Structured Text PLC code documentation into Sphinx. 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | :caption: Contents: 26 | 27 | src/quickstart 28 | src/config 29 | src/directives 30 | src/examples 31 | src/modules 32 | src/limitations 33 | 34 | 35 | ################## 36 | Indices and tables 37 | ################## 38 | 39 | * :ref:`genindex` 40 | * :ref:`modindex` 41 | * :ref:`search` 42 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/plc_code/.gitignore: -------------------------------------------------------------------------------- 1 | # IDE stuff 2 | *.bak 3 | .vs 4 | *.suo 5 | *.project.~u 6 | *.user 7 | *.dbg 8 | 9 | # TwinCAT files 10 | *.tmc 11 | TrialLicense.tclrs 12 | _CompileInfo/ 13 | _ModuleInstall/ 14 | _Boot/ 15 | _Libraries/ 16 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/DUTs/ST_ExampleStruct.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/GVLs/GVL_Main.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/MyPlcProject.plcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0.0 4 | 2.0 5 | {96c34c3a-c6fd-4068-b849-bc29b4aaf54b} 6 | True 7 | true 8 | true 9 | false 10 | MyPlcProject 11 | 3.1.4023.0 12 | {3b92c4d8-809e-4a08-a573-212a4614c1d2} 13 | {2906b746-0b48-489c-a1c6-6da565eb79ff} 14 | {f9d7bf8b-7bed-46b9-8427-e4e8253572ff} 15 | {0f28e751-7d97-491f-8032-d999394cec85} 16 | {f43f87b4-45cc-4fb0-b38b-dc3863acebae} 17 | {7e87d3f9-845b-4369-a856-b933ccd0e49c} 18 | 19 | 20 | 21 | Code 22 | 23 | 24 | Code 25 | true 26 | 27 | 28 | Code 29 | 30 | 31 | Code 32 | 33 | 34 | Code 35 | 36 | 37 | Code 38 | 39 | 40 | Code 41 | 42 | 43 | Code 44 | 45 | 46 | Code 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Tc2_Standard, * (Beckhoff Automation GmbH) 59 | Tc2_Standard 60 | 61 | 62 | Tc2_System, * (Beckhoff Automation GmbH) 63 | Tc2_System 64 | 65 | 66 | Tc3_Module, * (Beckhoff Automation GmbH) 67 | Tc3_Module 68 | 69 | 70 | 71 | 72 | Content 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | "<ProjectRoot>" 81 | 82 | 83 | 84 | 85 | 86 | System.Collections.Hashtable 87 | {54dd0eac-a6d8-46f2-8c27-2f43c7e49861} 88 | System.String 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/ExampleFolder/FB_ExampleFolder_FunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/ExampleFolder/F_ExampleFolder_Function.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/ExampleFolder/ST_ExampleFolder_Struct.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/FB_ExampleFunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/F_ExampleFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/POUs/MAIN.TcPOU: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/MyPlcProject/PlcTask.TcTTO: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10000 6 | 20 7 | 8 | MAIN 9 | 10 | {88cfd855-2423-4050-83bc-1e302b505476} 11 | {3e8c0678-d8f1-4b6e-a89f-d4db772a3f53} 12 | {2c990b1c-2e98-4b39-b8a4-291f37c7d92c} 13 | {c61af8e7-d675-49ef-81e5-8733f963b390} 14 | {5b427e69-b318-402e-951b-17d29ee6a86e} 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/TwinCAT Project.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.33927.289 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{B1E792BE-AA5F-4E3C-8C82-674BF9C0715B}") = "TwinCAT Project", "TwinCAT Project.tsproj", "{D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|TwinCAT CE7 (ARMV7) = Debug|TwinCAT CE7 (ARMV7) 11 | Debug|TwinCAT OS (ARMT2) = Debug|TwinCAT OS (ARMT2) 12 | Debug|TwinCAT RT (x64) = Debug|TwinCAT RT (x64) 13 | Debug|TwinCAT RT (x86) = Debug|TwinCAT RT (x86) 14 | Release|TwinCAT CE7 (ARMV7) = Release|TwinCAT CE7 (ARMV7) 15 | Release|TwinCAT OS (ARMT2) = Release|TwinCAT OS (ARMT2) 16 | Release|TwinCAT RT (x64) = Release|TwinCAT RT (x64) 17 | Release|TwinCAT RT (x86) = Release|TwinCAT RT (x86) 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7) 21 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7) 22 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2) 23 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2) 24 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64) 25 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64) 26 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86) 27 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86) 28 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7) 29 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7) 30 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2) 31 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2) 32 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64) 33 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64) 34 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86) 35 | {D7CE8978-BC8A-4D23-B614-7BEA6E2AAAB7}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86) 36 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7) 37 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7) 38 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2) 39 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2) 40 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64) 41 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64) 42 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86) 43 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86) 44 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7) 45 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7) 46 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2) 47 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2) 48 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64) 49 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64) 50 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86) 51 | {96C34C3A-C6FD-4068-B849-BC29B4AAF54B}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86) 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {979B5B36-8B9A-44FC-AB7D-B3614737BC2F} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /docs/plc_code/TwinCAT Project/TwinCAT Project.tsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PlcTask 8 | 9 | 10 | 11 | 12 | 13 | 14 | MyPlcProject Instance 15 | {08500001-0000-0000-F000-000000000064} 16 | 17 | 18 | 0 19 | PlcTask 20 | 21 | #x02010030 22 | 23 | 20 24 | 10000000 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/src/config.rst: -------------------------------------------------------------------------------- 1 | ################ 2 | Config Variables 3 | ################ 4 | -------------------------------------------------------------------------------- /docs/src/directives.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Directives 3 | ########## 4 | 5 | .. default-domain:: plc 6 | 7 | .. note:: All ``plc:`` domain prefixes are omitted here! 8 | 9 | Manual Directives 10 | ================= 11 | 12 | Use manual directives to describe PLC objects, without referencing to real source code. 13 | 14 | function 15 | -------- 16 | 17 | .. code-block:: rst 18 | 19 | .. function:: 20 | :var_in : 21 | :var_out : 22 | :var_in_out : 23 | :var_in : 24 | :returnvalue: 25 | :returntype: 26 | 27 | **Examples:** 28 | 29 | Take in a function name with real signature: 30 | 31 | .. code-block:: rst 32 | 33 | .. function:: Atan2(y: LREAL, x: LREAL) : LREAL 34 | 35 | Get 4-quadrant arc-tangent in degrees. 36 | 37 | .. function:: Atan2(y: LREAL, x: LREAL) : LREAL 38 | :noindex: 39 | 40 | Get 4-quadrant arc-tangent in degrees. 41 | 42 | You can also use directive parameters to describe your object: 43 | 44 | .. code-block:: rst 45 | 46 | .. function:: Atan2() 47 | 48 | :var_in LREAL y: Vertical distance 49 | :var_in LREAL x: Horizontal distance 50 | :rtype: LREAL 51 | 52 | Get 4-quadrant arc-tangent in degrees. 53 | 54 | .. function:: Atan2() 55 | :noindex: 56 | 57 | :var_in LREAL y: Vertical distance 58 | :var_in LREAL x: Horizontal distance 59 | :rtype: LREAL 60 | 61 | Get 4-quadrant arc-tangent in degrees. 62 | 63 | 64 | functionblock 65 | ------------- 66 | 67 | .. code-block:: rst 68 | 69 | .. functionblock:: 70 | <...> 71 | 72 | 73 | 74 | The same options from `function <#function>`_ are available. 75 | 76 | **Examples:** 77 | 78 | .. code-block:: rst 79 | 80 | .. functionblock:: MyFunctionBlock 81 | 82 | :var_input LREAL myInput: 83 | :var_output LREAL myOutput: 84 | 85 | Description of my function block. 86 | 87 | .. functionblock:: MyFunctionBlock 88 | :noindex: 89 | 90 | :var_input LREAL myInput: 91 | :var_output LREAL myOutput: 92 | 93 | Description of my function block. 94 | 95 | You can also nest e.g. methods and properties: 96 | 97 | .. code-block:: rst 98 | 99 | .. functionblock:: MyFunctionBlock 100 | :noindex: 101 | 102 | .. method:: MyMethod(input: BOOL) : STRING 103 | 104 | .. property:: Parameter : LREAL 105 | 106 | .. functionblock:: MyFunctionBlock 107 | :noindex: 108 | 109 | .. method:: MyMethod(input: BOOL) : STRING 110 | :noindex: 111 | 112 | .. property:: Parameter : LREAL 113 | :noindex: 114 | 115 | 116 | method 117 | ------ 118 | 119 | .. code-block:: rst 120 | 121 | .. method:: 122 | <...> 123 | 124 | The same options from `function <#function>`_ are available. 125 | 126 | 127 | property 128 | -------- 129 | 130 | .. code-block:: rst 131 | 132 | .. property:: 133 | 134 | **Examples:** 135 | 136 | .. code:: rst 137 | 138 | .. property:: someProp : BOOL 139 | 140 | .. property:: someProp : BOOL 141 | :noindex: 142 | 143 | 144 | enum / enumerator 145 | ----------------- 146 | 147 | .. code-block:: rst 148 | 149 | .. enum:: 150 | 151 | .. enumerator:: 152 | 153 | **Examples:** 154 | 155 | It is common to immediately next the possible values: 156 | 157 | .. code-block:: rst 158 | 159 | .. enum:: Color 160 | 161 | .. enumerator:: \ 162 | BLUE 163 | RED 164 | GREEN 165 | 166 | .. enum:: Color 167 | :noindex: 168 | 169 | .. enumerator:: \ 170 | BLUE 171 | RED 172 | GREEN 173 | 174 | 175 | struct 176 | ------ 177 | 178 | .. code-block:: rst 179 | 180 | .. struct:: 181 | 182 | .. member:: 183 | 184 | **Examples:** 185 | 186 | .. code-block:: rst 187 | 188 | .. struct:: Duration 189 | 190 | .. member:: Hour : UDINT 191 | .. member:: Minute : USINT 192 | .. member:: Second : LREAL 193 | 194 | .. struct:: Duration 195 | :noindex: 196 | 197 | .. member:: Hour : UDINT 198 | :noindex: 199 | .. member:: Minute : USINT 200 | :noindex: 201 | .. member:: Second : LREAL 202 | :noindex: 203 | 204 | gvl 205 | --- 206 | 207 | .. code-block:: rst 208 | 209 | .. gvl:: 210 | 211 | 212 | 213 | **Examples:** 214 | 215 | .. code-block:: rst 216 | 217 | .. gvl:: GVL_Main 218 | 219 | :var LREAL var_double: Some precision decimal variable 220 | :var BOOL my_flag: Another variable, of boolean type 221 | 222 | Main variables list. 223 | 224 | .. gvl:: GVL_Main 225 | :noindex: 226 | 227 | :var LREAL var_double: Some precision decimal variable 228 | :var BOOL my_flag: Another variable, of boolean type 229 | 230 | Main variables list. 231 | 232 | folder 233 | ------ 234 | 235 | .. code-block:: rst 236 | 237 | .. folder:: 238 | 239 | 240 | 241 | Use to indicate a group of objects belong together in a folder. 242 | 243 | **Examples:** 244 | 245 | .. code-block:: rst 246 | 247 | .. folder:: POUs/ExampleFolder 248 | 249 | .. function:: F_Example1 : BOOL 250 | .. function:: F_Example2 : BOOL 251 | 252 | .. folder:: POUs/ExampleFolder 253 | 254 | .. function:: F_Example1 : BOOL 255 | :noindex: 256 | 257 | .. function:: F_Example2 : BOOL 258 | :noindex: 259 | 260 | Auto Directives 261 | =============== 262 | 263 | Use auto directives to describe PLC objects through source code. 264 | Simply mention the object or a parent by name to retrieve the information. 265 | 266 | Relevant sources must be configured first. 267 | See :ref:`src/config:Config Variables` on how to do this. 268 | 269 | Most of the examples below are generated directly from ``docs/plc_code/TwinCAT Project/MyPlcProject``. 270 | 271 | autofunction 272 | ------------ 273 | 274 | .. code-block:: rst 275 | 276 | .. autofunction:: 277 | 278 | 279 | **Examples:** 280 | 281 | The following PLC declaration: 282 | 283 | .. code-block:: text 284 | 285 | (* 286 | This is an example function block. 287 | 288 | Here follows a longer description. 289 | *) 290 | FUNCTION F_ExampleFunction : LREAL 291 | VAR_INPUT 292 | inputVar1 : UDINT; // Description of first input 293 | inputVar2 : REAL := 3.14; // Description of second input 294 | END_VAR 295 | 296 | Might be displayed with: 297 | 298 | .. code-block:: rst 299 | 300 | .. autofunction:: F_ExampleFunction 301 | 302 | And the result looks like: 303 | 304 | .. autofunction:: F_ExampleFunction 305 | :noindex: 306 | 307 | autofunctionblock 308 | ----------------- 309 | 310 | .. code-block:: rst 311 | 312 | .. autofunction:: 313 | 314 | 315 | **Examples:** 316 | 317 | The following PLC declarations: (in TwinCAT this is really split between XML nodes) 318 | 319 | .. code-block:: text 320 | 321 | (* 322 | Here we describe a function block, including some children. 323 | *) 324 | FUNCTION_BLOCK FB_ExampleFunctionBlock 325 | VAR_INPUT 326 | input : BOOL; 327 | END_VAR 328 | VAR_OUTPUT 329 | output : STRING(15); 330 | END_VAR 331 | 332 | (* 333 | Child method of our FB. 334 | *) 335 | METHOD ExampleMethod 336 | VAR_INPUT 337 | END_VAR 338 | 339 | (* 340 | Reference to a variable that might be read-only. 341 | *) 342 | PROPERTY ExmapleProperty : REFERENCE TO LREAL 343 | 344 | Might be displayed with: 345 | 346 | .. code-block:: rst 347 | 348 | .. autofunctionblock:: FB_ExampleFunctionBlock 349 | :members: 350 | 351 | And the result looks like: 352 | 353 | .. autofunctionblock:: FB_ExampleFunctionBlock 354 | :members: 355 | 356 | automethod 357 | ---------- 358 | 359 | .. code-block:: rst 360 | 361 | .. automethod:: . 362 | 363 | Specific methods (of function blocks) can also be built directly, without their parent. 364 | Use a dot to include the parent name. 365 | 366 | **Examples:** 367 | 368 | The method above could be rendered stand-alone with: 369 | 370 | .. code-block:: rst 371 | 372 | .. automethod:: FB_ExampleFunctionBlock.ExampleMethod 373 | 374 | And the result would look like: 375 | 376 | .. automethod:: FB_ExampleFunctionBlock.ExampleMethod 377 | :noindex: 378 | 379 | autoproperty 380 | ------------ 381 | 382 | .. code-block:: rst 383 | 384 | .. autoproperty:: . 385 | 386 | Like methods, properties can also be built alone (see :ref:`src/directives:automethod`). 387 | 388 | **Examples:** 389 | 390 | The property above could be rendered stand-alone with: 391 | 392 | .. code-block:: rst 393 | 394 | .. autoproperty:: FB_ExampleFunctionBlock.ExampleProperty 395 | 396 | And the result would look like: 397 | 398 | .. autoproperty:: FB_ExampleFunctionBlock.ExampleProperty 399 | :noindex: 400 | 401 | autostruct 402 | ---------- 403 | 404 | .. code-block:: rst 405 | 406 | .. autostruct:: 407 | 408 | **Examples:** 409 | 410 | .. code-block:: rst 411 | 412 | .. autostruct:: ST_ExampleStruct 413 | 414 | .. autostruct:: ST_ExampleStruct 415 | :noindex: 416 | 417 | autogvl 418 | ------- 419 | 420 | .. code-block:: rst 421 | 422 | .. autogvl:: 423 | 424 | **Examples:** 425 | 426 | .. code-block:: rst 427 | 428 | .. autogvl:: GVL_Main 429 | 430 | .. autogvl:: GVL_Main 431 | :noindex: 432 | 433 | autofolder 434 | ---------- 435 | 436 | .. code-block:: rst 437 | 438 | .. autofolder:: 439 | 440 | Use ``autofolder`` to create references for all contents of a folder. 441 | It can be useful to quickly create content for a module, or simply all project content. 442 | 443 | Note that PLC in TwinCAT has no concept of modules or packages. 444 | Namespace of objects are not affected by their location inside your PLC project. 445 | So this concept of folders is purely imagined by ``plc-doc``. 446 | 447 | **Examples:** 448 | 449 | For the example project, the following could be used: 450 | 451 | .. code-block:: rst 452 | 453 | .. autofolder:: POUs/ExampleFolder 454 | 455 | To render the following: 456 | 457 | .. autofolder:: POUs/ExampleFolder 458 | :noindex: 459 | 460 | Referencing 461 | =========== 462 | 463 | Refer to code objects as you would in other domains. 464 | 465 | **Example:** 466 | 467 | .. code-block:: rst 468 | 469 | .. plc:function:: F_ToReferenceTo() 470 | 471 | Link to :plc:func:`F_ToReferenceTo`. 472 | 473 | .. plc:function:: F_ToReferenceTo() 474 | 475 | Link to :plc:func:`F_ToReferenceTo`. 476 | -------------------------------------------------------------------------------- /docs/src/examples.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Examples 3 | ######## 4 | 5 | Examples are listed in :ref:`src/directives:Directives`, below each comment. 6 | 7 | For the auto* directives, the TwinCAT project used is in the doc sources: 8 | https://github.com/RobertoRoos/sphinx-plc/tree/master/docs/plc_code/TwinCAT%20Project 9 | -------------------------------------------------------------------------------- /docs/src/limitations.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | Limitations and Issues 3 | ###################### 4 | 5 | For an up-to-date insight on issues, see https://github.com/DEMCON/plcdoc . 6 | Please help maintaining this project by adding any new issues there. 7 | 8 | Grammar 9 | ======= 10 | 11 | Grammar refers to the parsing of PLC code. 12 | This is done through a language specification, custom made in TextX. 13 | And so there is no guarantee that all possible PLC code can be parsed without error. 14 | Nonetheless, much of the most occurring code will parse with no problems. 15 | 16 | Expressions 17 | ----------- 18 | 19 | With expressions we mean all the literally typed constructions, from a simple ``5`` or ``'Hello World!'`` to a more complex ``CONCAT(INT_TO_STRING(5 + 3 * 8), '...')``. 20 | 21 | Currently, expressions are not parsed recursively as they would be for a real interpreter. 22 | Instead, when an expression is expected the whole item is simply matched as a string. 23 | So in the following... 24 | 25 | .. code-block:: 26 | 27 | my_int : INT := 1 + 1; 28 | my_string : STRING := CONCAT("+", MY_CONST); 29 | 30 | ...the initial values are registered as just ``"1 + 1"`` and ``"CONCAT("+", MY_CONST)"``. 31 | This means e.g. variables are never recognized in variable initialization and won't be linked. 32 | Aside from this, the expression should be printed normally in your generated docs item. 33 | 34 | String Escape 35 | ------------- 36 | 37 | Because `expressions <#Expressions>`_ are typically matched until the next ``;``, this breaks when a literal semicolon appears in a string. 38 | This is avoided specifically for singular string expressions, but not for any expression: 39 | 40 | .. code-block:: 41 | 42 | str1 : STRING := 'Hi;'; // This will parse 43 | str2 : STRING := CONCAT('Hi', ';'); // This will cause a parsing error 44 | 45 | Workaround 46 | ^^^^^^^^^^ 47 | 48 | For now you might have to use ``$3B`` as a `string constant `_ as a replacement for the literal semicolon. 49 | Or introduce (const) variables to break up those expressions. 50 | -------------------------------------------------------------------------------- /docs/src/modules.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | .. autosummary:: 5 | :toctree: _autosummary 6 | :recursive: 7 | 8 | plcdoc 9 | -------------------------------------------------------------------------------- /docs/src/quickstart.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Quickstart 3 | ########## 4 | -------------------------------------------------------------------------------- /examples/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /examples/docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "TestProject" 10 | copyright = "2022, Robert Roos" 11 | author = "Robert Roos" 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = ["plcdoc", "sphinx.ext.autodoc"] 17 | 18 | templates_path = ["_templates"] 19 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 20 | 21 | plc_sources = [ 22 | # "../TwinCAT PLC/.TcPOU", 23 | "../../tests/plc_code/TwinCAT PLC/MyPLC/POUs/RegularFunction.TcPOU", 24 | "../../tests/plc_code/TwinCAT PLC/MyPLC/POUs/PlainFunctionBlock.TcPOU", 25 | "../../tests/plc_code/TwinCAT PLC/MyPLC/POUs/PlainFunction.TcPOU", 26 | "../../tests/plc_code/TwinCAT PLC/MyPLC/POUs/FB_MyBlock.TcPOU", 27 | ] 28 | 29 | # -- Options for HTML output ------------------------------------------------- 30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 31 | 32 | html_theme = "sphinx_rtd_theme" 33 | html_static_path = ["_static"] 34 | nitpicky = True 35 | -------------------------------------------------------------------------------- /examples/docs/index.rst: -------------------------------------------------------------------------------- 1 | .. TestProject documentation master file, created by 2 | sphinx-quickstart on Fri Nov 4 17:13:56 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to TestProject's documentation! 7 | ======================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | plc 14 | plc_auto 15 | other_languages 16 | -------------------------------------------------------------------------------- /examples/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/docs/other_languages.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Other Languages 3 | *************** 4 | 5 | Regular Python Examples 6 | ======================= 7 | 8 | .. py:function:: my_function 9 | 10 | 11 | .. py:function:: python_function(arg1: BaseClass) -> SomeClass 12 | 13 | This is a very cool function but then in Python. 14 | 15 | :param str arg1: Awesome parameter. 16 | 17 | 18 | .. py:function:: some_other_function(x: int, y: int, z: int) -> int 19 | 20 | :param int x: 21 | :param int y: 22 | :param int z: 23 | :rtype: int 24 | 25 | .. py:class:: BaseClass 26 | 27 | .. py:class:: SomeClass(const_arg: int) 28 | 29 | Bases: :py:class:`BaseClass` 30 | 31 | Documentation of a class 32 | 33 | :param x: An input 34 | 35 | .. py:method:: get_stuff() -> bool 36 | 37 | :returns: True if there is stuff 38 | 39 | .. py:property:: some_property 40 | 41 | :type: int 42 | 43 | 44 | Above is some info about for example :py:func:`my_function` 45 | 46 | .. py:function:: function_types_test(x, y, z: int, w) 47 | 48 | :param int x: Variable X 49 | :param y: Variable Y 50 | :type y: int 51 | :param z: Variable Z 52 | :param w: Variable W 53 | :type w: str 54 | 55 | 56 | Regular C++ Examples 57 | ==================== 58 | 59 | .. cpp:class:: BaseClass 60 | 61 | .. cpp:class:: MyClass : public BaseClass 62 | 63 | I am an inherited class 64 | 65 | .. cpp:function:: bool get_ready() const 66 | 67 | .. cpp:function:: bool my_cpp_function(const int& x) 68 | 69 | :param x: This is the only input to the function. 70 | 71 | .. cpp:function:: double function_with_types(int x, float y, bool z, MyClass my_object) 72 | 73 | .. cpp:struct:: MyStruct 74 | 75 | I am a structure. 76 | 77 | .. cpp:member:: int x 78 | 79 | .. cpp:member:: int y 80 | 81 | Reference to :cpp:func:`my_cpp_function`. 82 | -------------------------------------------------------------------------------- /examples/docs/plc.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Manually Typed PLC Commands 3 | *************************** 4 | 5 | 6 | Manually Typed PLC Functions 7 | ============================ 8 | 9 | .. plc:function:: TestTypes(a, b, c, d) 10 | 11 | :param UINT a: 12 | :param LREAL b: 13 | :param INT c: 14 | :param BOOL d: 15 | 16 | .. plc:function:: MyAtan2(y: LREAL, x: LREAL) : LREAL 17 | 18 | This is a regular function. 19 | 20 | .. plc:function:: TestArgTypes(arg1, arg2, arg3) 21 | 22 | :param arg1: Content Arg1 23 | :type arg1: HERPDERP 24 | :param SCHERP arg2: Content Arg2 25 | :param LREAL arg3: Content Arg3 26 | 27 | 28 | Manually Typed PLC Function Blocks 29 | ================================== 30 | 31 | .. plc:functionblock:: MyFunctionBlock(MyInput: LREAL) 32 | 33 | .. plc:functionblock:: MyFunctionBlock2(MyInput: LREAL, MyOutput: REAL, MyInputOutput: LREAL) 34 | 35 | .. plc:functionblock:: MyFunctionBlock3(SomeInput, OtherInput, Buffer, IsReady, HasError) 36 | 37 | This is a very cool function block! 38 | 39 | :var_in INT SomeInput: Description for SomeInput. 40 | :IN BOOL OtherInput: About OtherInput. 41 | :IN_OUT LREAL Buffer: 42 | :OUT BOOL IsReady: Whether it is ready. 43 | :OUT BOOL HasError: 44 | 45 | .. plc:functionblock:: MyExtendedFunctionBlock 46 | 47 | It has a base class! 48 | 49 | This should be a reference: :plc:funcblock:`MyFunctionBlock`. 50 | 51 | .. plc:functionblock:: FunctionBlockWithMethod 52 | 53 | This is some function block. 54 | 55 | .. plc:method:: SomeMethod() 56 | 57 | .. plc:method:: FunctionBlockWithMethod.MethodWithPrefix() 58 | 59 | .. plc:method:: ImaginedFunctionBlock.SomeMethodStandAlone() 60 | 61 | .. plc:functionblock:: FunctionBlockWithProperty 62 | 63 | This function block has properties, defined in multiple ways. 64 | 65 | .. plc:property:: Param : LREAL 66 | 67 | .. plc:property:: FunctionBlockWithProperty.ParamWithPrefix : LREAL 68 | 69 | .. plc:property:: ImaginedFunctionBlock.ParamStandAlone : LREAL 70 | 71 | 72 | Type Links 73 | ========== 74 | 75 | .. plc:function:: FunctionCustomTypes(input: E_Options) : ST_MyStruct2 76 | 77 | .. plc:function:: FunctionCustomTypes2 78 | 79 | :var_in E_Options input: 80 | :rtype: ST_MyStruct2 81 | 82 | 83 | Manually Typed PLC Enums 84 | ======================== 85 | 86 | .. plc:enum:: E_Options 87 | 88 | I am options 89 | 90 | .. plc:enum:: Orientation 91 | 92 | .. plc:enumerator:: \ 93 | FaceUp 94 | FaceDown 95 | 96 | I am an orientation. 97 | 98 | 99 | Manually Typed PLC Structs 100 | ========================== 101 | 102 | .. plc:struct:: ST_MyStruct 103 | 104 | I have properties! 105 | 106 | .. plc:struct:: ST_MyStruct2 107 | 108 | .. plc:member:: \ 109 | FaceUp 110 | FaceDown 111 | 112 | 113 | Manually Typed PLC GVLs 114 | ======================= 115 | 116 | .. plc:gvl:: GVL_MyGVL 117 | 118 | Global variable list with, well, global variables. 119 | 120 | :var UDINT tickInterval_us: PLC tick interval 121 | -------------------------------------------------------------------------------- /examples/docs/plc_auto.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | Auto Typed PLC Stuff 3 | ******************** 4 | 5 | .. .. plc:autofunctionblock:: FB_MyBlock 6 | 7 | .. plc:autofunction:: RegularFunction 8 | 9 | .. This should give a warning: 10 | .. plc:autofunction:: FunctionThatDoesNotExist 11 | 12 | .. plc:autofunction:: PlainFunction 13 | 14 | .. This should give a warning: 15 | .. plc:autofunction:: PlainFunctionBlock 16 | 17 | 18 | .. plc:autofunctionblock:: PlainFunctionBlock 19 | 20 | 21 | .. .. plc:automethod:: FB_MyBlock.MyMethod 22 | 23 | 24 | .. plc:autofunctionblock:: FB_MyBlock 25 | :members: 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "plcdoc" 7 | version = "0.0.1" 8 | authors = [ 9 | { name="Robert Roos", email="robert.soor@gmail.com" }, 10 | ] 11 | description = "A tool to create PLC documentation for Sphinx" 12 | readme = "README.md" 13 | requires-python = ">=3.9" 14 | license = {text = "BSD-3-Clause"} 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ] 20 | dependencies = [ 21 | "sphinx>=5.0,<7.0", 22 | "textX>=3.0", 23 | ] 24 | [project.optional-dependencies] 25 | test = [ 26 | "black>=23.0", 27 | "pytest>=6.0", 28 | "pytest-cov>=4.0", 29 | "flake8>=6.0", 30 | "flake8-bugbear>=23.0", 31 | ] 32 | doc = [ 33 | "sphinx_rtd_theme>=1.4", 34 | ] 35 | 36 | [project.urls] 37 | "Homepage" = "https://github.com/DEMCON/plcdoc" 38 | "Bug Tracker" = "https://github.com/DEMCON/plcdoc/issues" 39 | "Documentation" = "https://plc-doc.readthedocs.io/latest/" 40 | 41 | [tool.setuptools.package-data] 42 | "*" = ["*.tx"] 43 | -------------------------------------------------------------------------------- /src/plcdoc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extension for Sphinx to integrate TwinCAT PLC code. 3 | """ 4 | 5 | from sphinx.application import Sphinx 6 | from .extension import plcdoc_setup 7 | from .domain import StructuredTextDomain # noqa: F401 8 | 9 | 10 | def setup(app: Sphinx): 11 | """Initialize Sphinx extension.""" 12 | 13 | plcdoc_setup(app) 14 | -------------------------------------------------------------------------------- /src/plcdoc/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /src/plcdoc/auto_directives.py: -------------------------------------------------------------------------------- 1 | """Contains the directives used to extract info real source.""" 2 | 3 | from typing import List 4 | 5 | from sphinx.ext.autodoc.directive import ( 6 | AutodocDirective, 7 | DummyOptionSpec, 8 | DocumenterBridge, 9 | ) 10 | from sphinx.ext.autodoc.directive import ( 11 | process_documenter_options, 12 | parse_generated_content, 13 | ) 14 | from sphinx.util.docutils import Reporter 15 | from sphinx.util import logging 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class PlcAutodocDirective(AutodocDirective): 21 | """Base class for auto-directives for the PLC domain. 22 | 23 | It extends the ``autodoc`` extension auto-directive base class. 24 | 25 | It works as a dispatcher of Documenters. It invokes a Documenter on running. 26 | After the processing, it parses and returns the generated content by 27 | Documenter. The directive itself doesn't modify the content. 28 | 29 | The exact type is extracted from the full directive name and the appropriate 30 | documenter is selected based on it. 31 | """ 32 | 33 | option_spec = DummyOptionSpec() 34 | has_content = True 35 | required_arguments = 1 36 | optional_arguments = 0 37 | final_argument_whitespace = True 38 | 39 | def run(self) -> List: 40 | reporter: Reporter = self.state.document.reporter 41 | 42 | try: 43 | source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore 44 | except AttributeError: 45 | source, lineno = (None, None) 46 | 47 | logger.debug(f"[plcdoc] {source}:{lineno}: input:\n{self.block_text}") 48 | 49 | # Look up target Documenter 50 | objtype = self.name.replace("auto", "") # Remove prefix from directive name 51 | doccls = self.env.app.registry.documenters[objtype] 52 | 53 | # Process the options with the selected documenter's option_spec 54 | try: 55 | documenter_options = process_documenter_options( 56 | doccls, self.config, self.options 57 | ) 58 | except (KeyError, ValueError, TypeError) as exc: 59 | # an option is either unknown or has a wrong type 60 | logger.error( 61 | "An option to %s is either unknown or has an invalid value: %s" 62 | % (self.name, exc), 63 | location=(source, lineno), 64 | ) 65 | return [] 66 | 67 | # Generate output 68 | params = DocumenterBridge( 69 | self.env, reporter, documenter_options, lineno, self.state 70 | ) 71 | documenter = doccls(params, self.arguments[0]) 72 | documenter.generate(more_content=self.content) 73 | if not params.result: 74 | return [] 75 | 76 | logger.debug("[plcdoc] output:\n%s", "\n".join(params.result)) 77 | 78 | # Record all filenames as dependencies -- this will at least partially make 79 | # automatic invalidation possible 80 | for fn in params.record_dependencies: 81 | self.state.document.settings.record_dependencies.add(fn) 82 | 83 | result = parse_generated_content(self.state, params.result, documenter) 84 | return result 85 | -------------------------------------------------------------------------------- /src/plcdoc/common.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from docutils.nodes import Node, Text 4 | 5 | from sphinx.environment import BuildEnvironment 6 | from sphinx.addnodes import pending_xref 7 | 8 | from typing import Optional, List 9 | 10 | 11 | _builtin_types_re = re.compile( 12 | r""" 13 | (L?)REAL 14 | |BOOL 15 | |(U?)(S|D|L?)INT 16 | """, 17 | re.VERBOSE, 18 | ) 19 | 20 | 21 | def type_to_xref( 22 | target: str, env: Optional[BuildEnvironment] = None, suppress_prefix: bool = False 23 | ) -> pending_xref: 24 | """Convert a type string to a cross-reference node. 25 | 26 | This function is a direct mirror of :func:`python.type_to_xref`. 27 | """ 28 | kwargs = {} 29 | 30 | contnodes = [Text(target)] 31 | 32 | return pending_xref( 33 | "", 34 | *contnodes, 35 | refdomain="plc", 36 | reftype=None, 37 | reftarget=target, 38 | refspecific=False, 39 | **kwargs, 40 | ) 41 | 42 | 43 | def _parse_annotation(annotation: str, env: Optional[BuildEnvironment]) -> List[Node]: 44 | """Parse type annotation to e.g. a cross-reference 45 | 46 | This function is a direct mirror of :func:`python._parse_annotation`. 47 | """ 48 | if _builtin_types_re.match(annotation): 49 | return [] # Skip built-in types 50 | 51 | return [type_to_xref(annotation, env)] 52 | -------------------------------------------------------------------------------- /src/plcdoc/directives.py: -------------------------------------------------------------------------------- 1 | """Contains the static directives for the PLC domain. 2 | 3 | They are added into :class:`~plcdoc.StructuredTextDomain`, they are not registered 4 | manually inside the Sphinx :func:`~plcdoc.setup` callback. 5 | """ 6 | 7 | from typing import List, Tuple, TYPE_CHECKING 8 | 9 | from docutils import nodes 10 | from docutils.parsers.rst import directives 11 | 12 | from sphinx import addnodes 13 | from sphinx.directives import ObjectDescription 14 | from sphinx.util.docfields import Field, TypedField 15 | from sphinx.util.nodes import make_id 16 | from sphinx.domains.python import _pseudo_parse_arglist 17 | 18 | from .documenters import plc_signature_re 19 | from .common import _parse_annotation 20 | 21 | if TYPE_CHECKING: 22 | from .domain import StructuredTextDomain 23 | 24 | 25 | class PlcObjectDescription(ObjectDescription): 26 | """Base class for description directives (e.g. for `function`). 27 | 28 | :cvar has_arguments: ``True`` if this type of description has a list of arguments 29 | :cvar object_display_type: ``True``: determine automatically, ``False``: do not use, 30 | ``str``: use literal value 31 | """ 32 | 33 | has_arguments = False 34 | 35 | option_spec = { 36 | "noindex": directives.flag, 37 | "noindexentry": directives.flag, 38 | "module": directives.unchanged, 39 | } 40 | 41 | object_display_type = True 42 | 43 | allow_nesting = False 44 | 45 | def handle_signature( 46 | self, sig: str, signode: addnodes.desc_signature 47 | ) -> Tuple[str, str]: 48 | """Break down Structured Text signatures. 49 | 50 | This often won't be enough, because IN, OUT and IN_OUT variables are not defined 51 | with the block declaration. 52 | Further declaration will have to rely on commands like `:var_in:`. 53 | Even though not valid PLC syntax, an argument list after the name is processed 54 | in Python style. 55 | 56 | If inside a class (i.e. function block), the current class is handled like: 57 | * It is stripped from the displayed name if present 58 | * It is added to the full name if not present 59 | 60 | Like ``autodoc`` a nesting stack is tracked to know if we're currently inside a 61 | class. 62 | """ 63 | m = plc_signature_re.match(sig) 64 | if m is None: 65 | raise ValueError 66 | prefix, name, arglist, retann, extends = m.groups() 67 | 68 | # Check what class we are currently nested in 69 | classname = self.env.ref_context.get("plc:functionblock") 70 | if classname: 71 | if prefix and (prefix == classname or prefix.startswith(classname + ".")): 72 | # Classname was also typed in (ignore the dot at the end) 73 | fullname = prefix + name # E.g. "MyClass.some_method" 74 | prefix = "" # Remove classname from prefix, don't need it twice 75 | elif prefix: 76 | # A prefix was given but it's different from the nesting hierarchy 77 | fullname = classname + "." + prefix + name 78 | else: 79 | # No prefix given, rely on detected classname 80 | fullname = classname + "." + name 81 | else: 82 | if prefix: 83 | classname = prefix.rstrip(".") 84 | fullname = prefix + name 85 | else: 86 | classname = "" 87 | fullname = name 88 | 89 | signode["class"] = classname 90 | signode["fullname"] = fullname 91 | 92 | sig_prefix = self.get_signature_prefix(sig) 93 | if sig_prefix: 94 | signode += addnodes.desc_annotation(str(sig_prefix), "", *sig_prefix) 95 | 96 | if prefix: 97 | signode += addnodes.desc_addname(prefix, prefix) 98 | 99 | signode += addnodes.desc_name("", "", addnodes.desc_sig_name(name, name)) 100 | 101 | if self.has_arguments: 102 | # TODO: Add type link from annotation (like return type) 103 | if not arglist: 104 | signode += addnodes.desc_parameterlist() 105 | else: 106 | _pseudo_parse_arglist(signode, arglist) 107 | 108 | if retann: 109 | children = _parse_annotation(retann, self.env) 110 | signode += addnodes.desc_returns( 111 | retann, "" if children else retann, *children 112 | ) 113 | 114 | return fullname, prefix 115 | 116 | def get_signature_prefix(self, sig: str) -> List[nodes.Node]: 117 | """Return a prefix to put before the object name in the signature. 118 | 119 | E.g. "FUNCTION_BLOCK" or "METHOD". 120 | """ 121 | if self.object_display_type is True: 122 | objtype = self.objtype.upper() 123 | objtype = objtype.replace("BLOCK", "_BLOCK") 124 | elif self.object_display_type is not False: 125 | objtype = self.object_display_type 126 | else: 127 | return [] 128 | 129 | return [nodes.Text(objtype), addnodes.desc_sig_space()] 130 | 131 | def add_target_and_index( 132 | self, name: Tuple[str, str], sig: str, signode: addnodes.desc_signature 133 | ) -> None: 134 | """Add cross-reference IDs and entries to self.indexnode, if applicable.""" 135 | fullname = name[0] 136 | node_id = make_id(self.env, self.state.document, "", fullname) 137 | signode["ids"].append(node_id) 138 | self.state.document.note_explicit_target(signode) 139 | 140 | domain: "StructuredTextDomain" = self.env.get_domain("plc") 141 | domain.note_object(fullname, self.objtype, node_id, location=signode) 142 | 143 | def before_content(self) -> None: 144 | """Called before parsing content. 145 | 146 | For nested objects (like methods inside a function block), this method will 147 | build up a stack of the nesting hierarchy. 148 | """ 149 | prefix = None 150 | if self.names: 151 | # fullname and name_prefix come from the `handle_signature` method. 152 | # fullname represents the full object name that is constructed using 153 | # object nesting and explicit prefixes. `name_prefix` is the 154 | # explicit prefix given in a signature 155 | (fullname, name_prefix) = self.names[-1] 156 | if self.allow_nesting: 157 | prefix = fullname 158 | elif name_prefix: 159 | prefix = name_prefix.strip(".") 160 | if prefix: 161 | self.env.ref_context["plc:functionblock"] = prefix 162 | if self.allow_nesting: 163 | classes = self.env.ref_context.setdefault("plc:functionblocks", []) 164 | classes.append(prefix) 165 | 166 | def after_content(self) -> None: 167 | """Called after creating the content through nested parsing. 168 | 169 | Used to handle de-nesting the hierarchy. 170 | """ 171 | classes = self.env.ref_context.setdefault("plc:functionblocks", []) 172 | if self.allow_nesting: 173 | try: 174 | classes.pop() 175 | except IndexError: 176 | pass 177 | 178 | self.env.ref_context["plc:functionblock"] = ( 179 | classes[-1] if len(classes) > 0 else None 180 | ) 181 | 182 | 183 | class PlcCallableDescription(PlcObjectDescription): 184 | """Directive to describe a callable object (function, function block).""" 185 | 186 | has_arguments = True 187 | 188 | # fmt: off 189 | doc_field_types = [ 190 | TypedField( 191 | "var_in", 192 | label="VAR_IN", 193 | names=("var_in", "VAR_IN", "var_input", "VAR_INPUT", "in", "IN", "param", 194 | "parameter", "arg", "argument"), 195 | typerolename="type", 196 | typenames=("paramtype", "type", "var_in_type", "type_in"), 197 | can_collapse=False, 198 | ), 199 | TypedField( 200 | "var_out", 201 | label="VAR_OUT", 202 | names=("var_out", "VAR_OUT", "var_output", "VAR_OUTPUT", "out", "OUT"), 203 | typerolename="type", 204 | typenames=("var_out_type", "type_out"), # Just "type" cannot be re-used! 205 | can_collapse=False, 206 | ), 207 | TypedField( 208 | "var_in_out", 209 | label="VAR_IN_OUT", 210 | names=("var_in_out", "VAR_IN_OUT", "var_input_output", "VAR_INPUT_OUTPUT", 211 | "in_out", "IN_OUT"), 212 | typerolename="type", 213 | typenames=("var_in_out_type", "type_in_out"), 214 | can_collapse=False, 215 | ), 216 | Field( 217 | "returnvalue", 218 | label="Returns", 219 | has_arg=False, 220 | names=("returnvalue", "returns", "return", "RETURNS", "RETURN"), 221 | ), 222 | Field( 223 | "returntype", 224 | label="Return type", 225 | has_arg=False, 226 | names=("returntype", "rtype",), bodyrolename="type"), 227 | ] 228 | # fmt: on 229 | 230 | 231 | class PlcFunctionBlockDescription(PlcCallableDescription): 232 | """Directive specifically for function blocks.""" 233 | 234 | allow_nesting = True 235 | 236 | 237 | class PlcEnumeratorDescription(PlcObjectDescription): 238 | """Directive for values of enums.""" 239 | 240 | object_display_type = False 241 | 242 | def add_target_and_index( 243 | self, name: Tuple[str, str], sig: str, signode: addnodes.desc_signature 244 | ) -> None: 245 | # Do not index 246 | # TODO: Fix indexing for enums 247 | return 248 | 249 | 250 | class PlcMemberDescription(PlcObjectDescription): 251 | """Directive specifically for (struct) members.""" 252 | 253 | object_display_type = False 254 | 255 | 256 | class PlcVariableListDescription(PlcObjectDescription): 257 | """Directive specifically to show a GVL.""" 258 | 259 | # fmt: off 260 | doc_field_types = [ 261 | TypedField( 262 | "var", 263 | label="VAR", 264 | names=("var", "param", "parameter", "arg", "argument"), 265 | typerolename="type", 266 | typenames=("paramtype", "type", "var_type"), 267 | can_collapse=False, 268 | ), 269 | ] 270 | # fmt: on 271 | 272 | 273 | class PlcFolderDescription(PlcObjectDescription): 274 | """Directive specifically for a folder and contents.""" 275 | 276 | allow_nesting = True 277 | 278 | def handle_signature( 279 | self, sig: str, signode: addnodes.desc_signature 280 | ) -> Tuple[str, str]: 281 | # Folder name does not require any parsing 282 | signode["fullname"] = sig 283 | 284 | sig_prefix = self.get_signature_prefix("folder") 285 | signode += addnodes.desc_annotation(str(sig_prefix), "", *sig_prefix) 286 | signode += addnodes.desc_name("", "", addnodes.desc_sig_name(sig, sig)) 287 | 288 | return sig, "" 289 | -------------------------------------------------------------------------------- /src/plcdoc/documenters.py: -------------------------------------------------------------------------------- 1 | """Contains the documenters used to bridge the source extracted data.""" 2 | 3 | import os.path 4 | from abc import ABC 5 | from typing import Tuple, List, Dict, Optional, Any, Union 6 | import re 7 | 8 | from sphinx.util import logging 9 | from sphinx.ext.autodoc import ( 10 | Documenter as AutodocDocumenter, 11 | members_option, 12 | ALL, 13 | ) 14 | from docutils.statemachine import StringList 15 | 16 | from .interpreter import PlcInterpreter, PlcDeclaration, TextXMetaClass 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | # Regex for unofficial PLC signatures -- this is used for non-auto 22 | # directives for example. 23 | plc_signature_re = re.compile( 24 | r"""^ ([\w.]*\.)? # class name(s) 25 | (\w+) \s* # thing name 26 | (?: 27 | (?:\(\s*(.*)\s*\))? # optional: arguments 28 | (?:\s* : \s* (.*))? # return annotation 29 | (?:\s* EXTENDS \s* (.*))? # extends 30 | )? $ # and nothing more 31 | """, 32 | re.VERBOSE, 33 | ) 34 | 35 | 36 | class PlcDocumenter(AutodocDocumenter, ABC): 37 | """Derived documenter base class for the PLC domain. 38 | 39 | These documenters are added to the registry in the extension :func:`~plcdoc.setup` 40 | callback. 41 | 42 | The purpose of a documenter is to generate literal reST code, that can be rendered 43 | into the docs, based on source code analysis. 44 | 45 | :cvar objtype: The object name as used for generating a directive 46 | (should be overriden by different types) 47 | """ 48 | 49 | domain = "plc" 50 | 51 | priority = 10 52 | 53 | @classmethod 54 | def can_document_member( 55 | cls, member: PlcDeclaration, membername: str, isattr: bool, parent: Any 56 | ) -> bool: 57 | """Test if this documenter is suitable to document a source object. 58 | 59 | This is used specifically when source analysis shows an object as members 60 | (= children), e.g. a FUNCTION_BLOCK with methods and properties. 61 | 62 | This base implementation simply relies on ``self.objtype``. 63 | """ 64 | if hasattr(member, "objtype"): 65 | return member.objtype == cls.objtype 66 | 67 | return False 68 | 69 | def get_object_members(self, want_all: bool) -> Tuple: 70 | """Method used by ``autodoc`` get object members, not used here.""" 71 | return False, [] 72 | 73 | def generate( 74 | self, 75 | more_content: Optional[StringList] = None, 76 | real_modname: str = None, 77 | check_module: bool = False, 78 | all_members: bool = False, 79 | ) -> None: 80 | """Generate reST for the object given by ``self.name``.""" 81 | if not self.parse_name(): 82 | logger.warning(f"Failed to parse name `{self.name}`") 83 | return 84 | 85 | if not self.import_object(): 86 | return 87 | 88 | # Make sure that the result starts with an empty line. This is 89 | # necessary for some situations where another directive preprocesses 90 | # reST and no starting newline is present 91 | self.add_line("", "") 92 | 93 | # Format the object's signature, if any 94 | sig = self.format_signature() 95 | 96 | # Generate the directive header and options, if applicable 97 | self.add_directive_header(sig) 98 | self.add_line("", "") # Blank line again 99 | 100 | # E.g. the module directive doesn't have content 101 | self.indent += self.content_indent 102 | 103 | # Add all content (from docstrings, attribute docs etc.) 104 | self.add_content(more_content) 105 | 106 | # Document members, if possible 107 | self.document_members(all_members) 108 | 109 | def parse_name(self) -> bool: 110 | """Determine the full name of the target and what modules to import. 111 | 112 | We are going to get info from already processed content, so we don't actually 113 | have to import any files (this is terminology from the original ``autodoc``). 114 | 115 | Sets the properties `fullname`, `modname`, `retann`, `args` 116 | """ 117 | try: 118 | # Parse the name supplied as directive argument 119 | path, base, args, retann, extann = plc_signature_re.match( 120 | self.name 121 | ).groups() 122 | except AttributeError: 123 | logger.warning(f"Invalid signature for auto-{self.objtype} (f{self.name})") 124 | return False 125 | 126 | modname = None 127 | parents = [] 128 | 129 | self.modname, self.objpath = self.resolve_name(modname, parents, path, base) 130 | 131 | self.args = args 132 | self.retann = retann 133 | self.fullname = ".".join(self.objpath) 134 | 135 | return True 136 | 137 | def resolve_name( 138 | self, modname: str, parents: Any, path: str, base: Any 139 | ) -> Tuple[Optional[str], List[str]]: 140 | """Using the regex result, identify this object. 141 | 142 | Also use the environment if necessary. 143 | This is similar for most objects: there can be a class-like prefix followed by 144 | the name. 145 | """ 146 | if path: 147 | mod_cls = path.rstrip(".") 148 | else: 149 | # No full path is given, search in: 150 | # An autodoc directive 151 | mod_cls = self.env.temp_data.get("plc_autodoc:class") 152 | # Nested class-like directive: 153 | if mod_cls is None: 154 | # TODO: Make sure `ref_context` can actually work 155 | mod_cls = self.env.ref_context.get("plc:functionblock") 156 | # Cannot be found at all 157 | if mod_cls is None: 158 | return None, parents + [base] 159 | 160 | _, sep, cls = mod_cls.rpartition(".") 161 | parents = [cls] 162 | 163 | return None, parents + [base] 164 | 165 | def format_name(self) -> str: 166 | """Get name to put in generated directive. 167 | 168 | Overriden from `autodoc` because have no `objpath` or `modname`. 169 | """ 170 | return self.name 171 | 172 | def format_args(self, **kwargs: Any) -> Optional[str]: 173 | """Format arguments for signature, based on auto-data.""" 174 | 175 | arg_strs = [f"{var.name}" for var in self.object.get_args()] 176 | 177 | return "(" + ", ".join(arg_strs) + ")" 178 | 179 | def import_object(self, raiseerror: bool = False) -> bool: 180 | """Imports the object given by ``self.modname``. 181 | 182 | Processing of source files is already done in :func:`analyse``, we look for the 183 | result here. 184 | In the original Python ``autodoc`` this is where target files are loaded and 185 | read. 186 | """ 187 | interpreter: PlcInterpreter = self.env.app._interpreter 188 | 189 | try: 190 | self.object: PlcDeclaration = interpreter.get_object( 191 | self.fullname, self.objtype 192 | ) 193 | except KeyError as err: 194 | logger.warning(err) 195 | return False 196 | 197 | return True 198 | 199 | def add_content(self, more_content: Optional[StringList]) -> None: 200 | """Add content from docstrings, attribute documentation and user.""" 201 | 202 | # Add docstring from meta-model 203 | sourcename = self.get_sourcename() 204 | docstrings = self.get_doc() 205 | 206 | # Also add VARs from meta-model 207 | args_block = [] 208 | for var in self.object.get_args(): 209 | line_param = f":{var.kind} {var.type.name} {var.name}:" 210 | if var.comment and var.comment.text: 211 | line_param += " " + var.comment.text 212 | args_block.append(line_param) 213 | 214 | if args_block: 215 | docstrings.append(args_block) 216 | 217 | if docstrings is not None: 218 | if not docstrings: # Empty array 219 | # Append at least a dummy docstring so the events are fired 220 | docstrings.append([]) 221 | for i, line in enumerate(self.process_doc(docstrings)): 222 | self.add_line(line, sourcename, i) 223 | 224 | # Add additional content (e.g. from document), if present 225 | if more_content: 226 | for line, src in zip(more_content.data, more_content.items): 227 | self.add_line(line, src[0], src[1]) 228 | 229 | def get_doc(self) -> Optional[List[List[str]]]: 230 | """Get docstring from the meta-model.""" 231 | 232 | # Read main docblock 233 | comment_str = self.object.get_comment() 234 | if not comment_str: 235 | return [] 236 | 237 | comment_lines = [line.strip() for line in comment_str.strip().split("\n")] 238 | 239 | return [comment_lines] 240 | 241 | def document_members(self, all_members: bool = False) -> None: 242 | """Create automatic documentation of members of the object. 243 | 244 | This includes methods, properties, etc. 245 | 246 | ``autodoc`` will skip undocumented members by default, we will document 247 | everything always. 248 | """ 249 | return None 250 | 251 | def get_member_documenter( 252 | self, child: PlcDeclaration 253 | ) -> Optional[AutodocDocumenter]: 254 | """Put together a documenter for a child. 255 | 256 | This method is not used out of the box - call it from :meth:`document_members`. 257 | """ 258 | # Find suitable documenters for this member 259 | classes = [ 260 | cls 261 | for documenter_name, cls in self.documenters.items() 262 | if documenter_name.startswith("plc:") 263 | and cls.can_document_member(child, child.name, True, self) 264 | ] 265 | if not classes: 266 | logger.warning( 267 | f"Could not found a suitable documenter for `{child.name}` " 268 | f"(`{child.objtype}`)" 269 | ) 270 | return None 271 | # Prefer the documenter with the highest priority 272 | classes.sort(key=lambda cls: cls.priority) 273 | return classes[-1](self.directive, child.name, self.indent) 274 | 275 | def get_sourcename(self) -> str: 276 | """Get origin of info for tracing purposes.""" 277 | return f"{self.object.file}:declaration of {self.fullname}" 278 | 279 | def get_object_children(self, want_all: bool) -> Dict[str, Any]: 280 | """Get list of children of self.object that overlap with the member settings.""" 281 | selected_children = {} 282 | 283 | if not want_all: 284 | if not self.options.members: 285 | return selected_children 286 | 287 | # Specific members given 288 | for name in self.options.members: 289 | if name in self.object.children: 290 | selected_children[name] = self.object.children[name] 291 | else: 292 | logger.warning(f"Cannot find {name} inside {self.fullname}") 293 | else: 294 | selected_children = self.object.children 295 | 296 | return selected_children 297 | 298 | 299 | class PlcFunctionDocumenter(PlcDocumenter): 300 | """Documenter for the plain Function type.""" 301 | 302 | objtype = "function" 303 | 304 | 305 | class PlcMethodDocumenter(PlcFunctionDocumenter): 306 | """Documenter for the Method type. 307 | 308 | This works both as a stand-alone directive as part of a function block. 309 | """ 310 | 311 | objtype = "method" 312 | priority = PlcFunctionDocumenter.priority + 1 313 | # Methods and Functions can be documented the same, but we should prefer a method 314 | # when possible 315 | 316 | 317 | class PlcFunctionBlockDocumenter(PlcFunctionDocumenter): 318 | """Documenter for the Function Block type.""" 319 | 320 | objtype = "functionblock" 321 | 322 | option_spec = { 323 | "members": members_option, 324 | } 325 | 326 | def document_members(self, all_members: bool = False) -> None: 327 | """Document nested members.""" 328 | # TODO: Add documenting for members 329 | 330 | # Set current namespace for finding members 331 | self.env.temp_data["plc_autodoc:module"] = self.modname 332 | if self.objpath: 333 | self.env.temp_data["plc_autodoc:class"] = self.objpath[0] 334 | 335 | want_all = ( 336 | all_members or self.options.inherited_members or self.options.members is ALL 337 | ) 338 | 339 | member_documenters = [ 340 | (self.get_member_documenter(child), False) 341 | for child in self.get_object_children(want_all).values() 342 | ] 343 | 344 | # TODO: Sort members 345 | 346 | for documenter, _ in member_documenters: 347 | if documenter: 348 | documenter.generate( 349 | all_members=True, 350 | real_modname="", 351 | check_module=False, 352 | ) 353 | 354 | # Reset context 355 | self.env.temp_data["plc_autodoc:module"] = None 356 | self.env.temp_data["plc_autodoc:class"] = None 357 | 358 | 359 | class PlcDataDocumenter(PlcDocumenter): 360 | """Intermediate base class to be used for all data types (non-callables). 361 | 362 | E.g. structs, enums and properties should extend from this. 363 | """ 364 | 365 | def format_signature(self, **kwargs: Any) -> str: 366 | """ 367 | 368 | Overload signature to remove redundant function brackets. This is not the most 369 | neat solution, but it works while staying DRY. 370 | """ 371 | result = super().format_signature(**kwargs) 372 | 373 | if result == "()": 374 | return "" 375 | 376 | return result 377 | 378 | 379 | class PlcPropertyDocumenter(PlcDataDocumenter): 380 | """Document a functionblock Property.""" 381 | 382 | objtype = "property" 383 | 384 | 385 | class PlcStructDocumenter(PlcDataDocumenter): 386 | """Document a struct.""" 387 | 388 | objtype = "struct" 389 | 390 | def document_members(self, all_members: bool = False) -> None: 391 | """Add directives for the struct properties.""" 392 | 393 | member_documenters = [ 394 | PlcStructMemberDocumenter( 395 | self.directive, 396 | member.name, 397 | self.indent, 398 | parent=self.object, 399 | member=member, 400 | ) 401 | for member in self.object.members 402 | ] 403 | 404 | # TODO: Sort members 405 | 406 | for documenter in member_documenters: 407 | documenter.generate( 408 | all_members=True, 409 | real_modname="", 410 | check_module=False, 411 | ) 412 | 413 | # This work, but maybe a new documenter is better: 414 | # for member in self.object.members: 415 | # # TODO: Get proper member inserted 416 | # self.add_line(f".. member:: {member.name} : {member.type}", 417 | # self.object.file) 418 | 419 | 420 | class PlcStructMemberDocumenter(PlcDataDocumenter): 421 | """Document a struct member (field). 422 | 423 | This documenter is slightly different, because it does not receive a full 424 | :class:`PlcDeclaration`, instead it gets a bit of raw TextX output. 425 | """ 426 | 427 | # TODO: Remove this class? 428 | 429 | objtype = "member" 430 | 431 | def __init__( 432 | self, 433 | directive, 434 | name: str, 435 | indent: str = "", 436 | parent: PlcDeclaration = None, 437 | member: Optional[TextXMetaClass] = None, 438 | ) -> None: 439 | super().__init__(directive, name, indent) 440 | 441 | self.object = parent 442 | self.member = member 443 | 444 | @classmethod 445 | def can_document_member( 446 | cls, 447 | member: Union[PlcDeclaration, Any], 448 | membername: str, 449 | isattr: bool, 450 | parent: Any, 451 | ) -> bool: 452 | return type(member).__name__ == "Variable" 453 | # Note: a TextX variable class is passed, not a complete PlcDeclaration 454 | 455 | def import_object(self, raiseerror: bool = False) -> bool: 456 | return self.member is not None # Expect member through constructor 457 | 458 | def get_doc(self) -> Optional[List[List[str]]]: 459 | # Read main docblock 460 | if self.member is None or self.member.comment is None: 461 | return [] 462 | 463 | comment_str = self.member.comment.text 464 | if not comment_str: 465 | return [] 466 | 467 | return [[comment_str]] 468 | 469 | def format_signature(self, **kwargs: Any) -> str: 470 | if not self.member: 471 | return "" 472 | 473 | # Insert the known variable type 474 | return f" : {self.member.type.name}" 475 | 476 | 477 | class PlcFolderDocumenter(PlcDataDocumenter): 478 | """Document a folder and its contents.""" 479 | 480 | objtype = "folder" 481 | 482 | def parse_name(self) -> bool: 483 | # Input is in ``self.name`` 484 | self.modname = None 485 | self.objpath = self.name 486 | 487 | self.args = None 488 | self.retann = None 489 | self.fullname = self.name 490 | 491 | return True 492 | 493 | def format_signature(self, **kwargs: Any) -> str: 494 | return "" 495 | 496 | def get_sourcename(self) -> str: 497 | return f"{self.fullname}:folder" 498 | 499 | def add_content(self, more_content: Optional[StringList]) -> None: 500 | if more_content: 501 | for line, src in zip(more_content.data, more_content.items): 502 | self.add_line(line, src[0], src[1]) 503 | 504 | def import_object(self, raiseerror: bool = False) -> bool: 505 | """Override import to process the folder name.""" 506 | 507 | interpreter: PlcInterpreter = self.env.app._interpreter 508 | 509 | folder = os.path.normpath(self.fullname) 510 | folder.strip(os.sep) 511 | 512 | try: 513 | self._contents: List[PlcDeclaration] = interpreter.get_objects_in_folder( 514 | folder 515 | ) 516 | except KeyError as err: 517 | logger.warning(err) 518 | return False 519 | 520 | return True 521 | 522 | def document_members(self, all_members: bool = False) -> None: 523 | member_documenters = [ 524 | (self.get_member_documenter(child), False) for child in self._contents 525 | ] 526 | 527 | # TODO: Sort content 528 | 529 | for documenter, _ in member_documenters: 530 | if documenter: 531 | documenter.generate( 532 | all_members=True, 533 | real_modname="", 534 | check_module=False, 535 | ) 536 | 537 | 538 | class PlcVariableListDocumenter(PlcDataDocumenter): 539 | 540 | objtype = "gvl" 541 | 542 | def format_args(self, **kwargs: Any) -> Optional[str]: 543 | return "" # Do not add arguments like function call 544 | -------------------------------------------------------------------------------- /src/plcdoc/domain.py: -------------------------------------------------------------------------------- 1 | """Contains the new PLC domain.""" 2 | 3 | from typing import List, Dict, Tuple, Any, NamedTuple, Optional 4 | 5 | from docutils.nodes import Element 6 | 7 | from sphinx.addnodes import pending_xref 8 | from sphinx.domains import Domain, ObjType 9 | from sphinx.builders import Builder 10 | from sphinx.environment import BuildEnvironment 11 | from sphinx.util import logging 12 | from sphinx.util.nodes import make_refnode, find_pending_xref_condition 13 | 14 | from .directives import ( 15 | PlcCallableDescription, 16 | PlcFunctionBlockDescription, 17 | PlcObjectDescription, 18 | PlcEnumeratorDescription, 19 | PlcMemberDescription, 20 | PlcFolderDescription, 21 | PlcVariableListDescription, 22 | ) 23 | from .roles import PlcXRefRole 24 | 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class ObjectEntry(NamedTuple): 30 | docname: str 31 | node_id: str 32 | objtype: str 33 | 34 | 35 | class StructuredTextDomain(Domain): 36 | """Sphinx domain for the PLC language. 37 | 38 | This provides a namespace for the new PLC-specific directives, and a way of 39 | describing a new syntax. 40 | """ 41 | 42 | name = "plc" 43 | label = "PLC (Structured Text)" 44 | 45 | # fmt: off 46 | # Objects are all the things that Sphinx can document 47 | object_types = { 48 | "function": ObjType("function", "func"), 49 | "method": ObjType("method", "meth"), 50 | "functionblock": ObjType("functionblock", "funcblock", "type"), 51 | "struct": ObjType("struct", "struct", "type"), 52 | "enum": ObjType("enum", "enum", "type"), 53 | "enumerator": ObjType("enumerator", "enumerator"), 54 | } 55 | 56 | directives = { 57 | "function": PlcCallableDescription, 58 | "functionblock": PlcFunctionBlockDescription, 59 | "method": PlcCallableDescription, 60 | "enum": PlcObjectDescription, 61 | "enumerator": PlcEnumeratorDescription, 62 | "struct": PlcObjectDescription, 63 | "member": PlcMemberDescription, 64 | "property": PlcObjectDescription, 65 | "gvl": PlcVariableListDescription, 66 | "folder": PlcFolderDescription, 67 | } 68 | 69 | # Roles are used to reference objects and are used like :rolename:`content` 70 | roles = { 71 | "func": PlcXRefRole(), 72 | "meth": PlcXRefRole(), 73 | "funcblock": PlcXRefRole(), 74 | "struct": PlcXRefRole(), 75 | "enum": PlcXRefRole(), 76 | "enumerator": PlcXRefRole(), 77 | "type": PlcXRefRole(), 78 | } 79 | 80 | # fmt: on 81 | 82 | initial_data = {"objects": {}, "modules": {}} 83 | 84 | indices = [] 85 | 86 | @property 87 | def objects(self) -> Dict[str, ObjectEntry]: 88 | return self.data.setdefault("objects", {}) # fullname -> ObjectEntry 89 | 90 | def note_object( 91 | self, 92 | name: str, 93 | objtype: str, 94 | node_id: str, 95 | location: Any = None, 96 | ) -> None: 97 | """Note an object for cross reference.""" 98 | if name in self.objects: # Duplicated 99 | other = self.objects[name] 100 | logger.warning( 101 | f"Duplicate object description of {name}, other instance in " 102 | f"{other.docname}, use :noindex: for one of them", 103 | location=location, 104 | ) 105 | 106 | self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype) 107 | 108 | def find_obj( 109 | self, 110 | env: BuildEnvironment, 111 | modname: Optional[str], 112 | classname: Optional[str], 113 | name: str, 114 | typ: Optional[str], 115 | searchmode: int = 0, 116 | ) -> List[Tuple[str, ObjectEntry]]: 117 | """Find an object for "name" in the database of objects. 118 | 119 | Returns a list of (name, object entry) tuples. 120 | 121 | The implementation is almost identical to :meth:`PythonDomain.find_obj`. 122 | 123 | If `searchmode` is equal to 1, the search is relaxed. The full path does not 124 | needto be specified. 125 | If `searchmode` is 0, only the full path is checked. 126 | """ 127 | if name[-2:] == "()": 128 | name = name[:-2] 129 | 130 | if not name: 131 | return [] 132 | 133 | matches: List[Tuple[str, ObjectEntry]] = [] 134 | 135 | newname = None 136 | if searchmode == 1: 137 | if typ is None: 138 | objtypes = list(self.object_types) 139 | else: 140 | objtypes = self.objtypes_for_role(typ) 141 | if objtypes is not None: 142 | if modname and classname: 143 | fullname = modname + "." + classname + "." + name 144 | if ( 145 | fullname in self.objects 146 | and self.objects[fullname].objtype in objtypes 147 | ): 148 | newname = fullname 149 | if not newname: 150 | if ( 151 | modname 152 | and modname + "." + name in self.objects 153 | and self.objects[modname + "." + name].objtype in objtypes 154 | ): 155 | newname = modname + "." + name 156 | elif ( 157 | name in self.objects and self.objects[name].objtype in objtypes 158 | ): 159 | newname = name 160 | else: 161 | # "fuzzy" searching mode 162 | searchname = "." + name 163 | matches = [ 164 | (oname, self.objects[oname]) 165 | for oname in self.objects 166 | if oname.endswith(searchname) 167 | and self.objects[oname].objtype in objtypes 168 | ] 169 | else: 170 | # NOTE: searching for exact match, object type is not considered 171 | if name in self.objects: 172 | newname = name 173 | elif typ == "mod": 174 | # only exact matches allowed for modules 175 | return [] 176 | elif classname and classname + "." + name in self.objects: 177 | newname = classname + "." + name 178 | elif modname and modname + "." + name in self.objects: 179 | newname = modname + "." + name 180 | elif ( 181 | modname 182 | and classname 183 | and modname + "." + classname + "." + name in self.objects 184 | ): 185 | newname = modname + "." + classname + "." + name 186 | if newname is not None: 187 | matches.append((newname, self.objects[newname])) 188 | return matches 189 | 190 | def resolve_xref( 191 | self, 192 | env: BuildEnvironment, 193 | fromdocname: str, 194 | builder: Builder, 195 | typ: str, 196 | target: str, 197 | node: pending_xref, 198 | contnode: Element, 199 | ) -> Optional[Element]: 200 | """Resolve cross-reference. 201 | 202 | Returns a reference node. 203 | 204 | The implementation is almost identical to :meth:`PythonDomain.resolve_xref`. 205 | """ 206 | modname = None 207 | clsname = None 208 | searchmode = 1 if node.hasattr("refspecific") else 0 209 | matches = self.find_obj(env, modname, clsname, target, typ, searchmode) 210 | 211 | if not matches: 212 | return None 213 | elif len(matches) > 1: 214 | logger.warning( 215 | "[plcdoc] more than one target found for cross-reference %r: %s", 216 | target, 217 | ", ".join(match[0] for match in matches), 218 | type="ref", 219 | subtype="python", 220 | location=node, 221 | ) 222 | 223 | name, obj = matches[0] 224 | 225 | if obj[2] == "module": 226 | # get additional info for modules 227 | # TODO: Reference to module 228 | return None 229 | else: 230 | return make_refnode(builder, fromdocname, obj[0], name, contnode, name) 231 | 232 | def resolve_any_xref( 233 | self, 234 | env: BuildEnvironment, 235 | fromdocname: str, 236 | builder: Builder, 237 | target: str, 238 | node: pending_xref, 239 | contnode: Element, 240 | ) -> List[Tuple[str, Element]]: 241 | """Resolve cross reference but without a known type.""" 242 | modname = None 243 | clsname = None 244 | results: List[Tuple[str, Element]] = [] 245 | 246 | # always search in "refspecific" mode with the :any: role 247 | matches = self.find_obj(env, modname, clsname, target, typ=None, searchmode=1) 248 | 249 | for name, obj in matches: 250 | if obj[2] == "module": 251 | pass # TODO: Catch modules 252 | else: 253 | # determine the content of the reference by conditions 254 | content = find_pending_xref_condition(node, "resolved") 255 | if content: 256 | children = content.children 257 | else: 258 | # if not found, use contnode 259 | children = [contnode] 260 | 261 | results.append( 262 | ( 263 | "plc:" + self.role_for_objtype(obj[2]), 264 | make_refnode( 265 | builder, fromdocname, obj[0], obj[1], children, name 266 | ), 267 | ) 268 | ) 269 | return results 270 | 271 | def merge_domaindata(self, docnames: List[str], otherdata: Dict): 272 | pass 273 | -------------------------------------------------------------------------------- /src/plcdoc/extension.py: -------------------------------------------------------------------------------- 1 | """Contains the technical Sphinx extension stuff.""" 2 | 3 | from typing import Dict, Optional 4 | import logging 5 | from sphinx.application import Sphinx 6 | from docutils.nodes import Element 7 | from sphinx.addnodes import pending_xref 8 | from sphinx.environment import BuildEnvironment 9 | 10 | from .__version__ import __version__ 11 | from .interpreter import PlcInterpreter 12 | from .domain import StructuredTextDomain 13 | from .auto_directives import PlcAutodocDirective 14 | from .documenters import ( 15 | PlcFunctionBlockDocumenter, 16 | PlcFunctionDocumenter, 17 | PlcMethodDocumenter, 18 | PlcPropertyDocumenter, 19 | PlcStructDocumenter, 20 | PlcStructMemberDocumenter, 21 | PlcFolderDocumenter, 22 | PlcVariableListDocumenter, 23 | ) 24 | from .common import _builtin_types_re 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def plcdoc_setup(app: Sphinx) -> Dict: 30 | """Initialize the plcdoc extension. 31 | 32 | Real setup function is put in the module ``__init__``. 33 | """ 34 | 35 | # We place a callback for Sphinx for when the builder is about ready to start to 36 | # index the PLC files. The moment of reading the PLC files could probably be 37 | # anything. 38 | 39 | app.setup_extension("sphinx.ext.autodoc") # Require the autodoc extension 40 | 41 | app.connect("builder-inited", analyze) 42 | 43 | app.add_config_value("plc_sources", [], True) # List[str] 44 | app.add_config_value("plc_project", None, True) # str 45 | 46 | app.add_domain(StructuredTextDomain) 47 | 48 | app.registry.add_documenter("plc:function", PlcFunctionDocumenter) 49 | app.add_directive_to_domain("plc", "autofunction", PlcAutodocDirective) 50 | 51 | app.registry.add_documenter("plc:functionblock", PlcFunctionBlockDocumenter) 52 | app.add_directive_to_domain("plc", "autofunctionblock", PlcAutodocDirective) 53 | 54 | app.registry.add_documenter("plc:method", PlcMethodDocumenter) 55 | app.add_directive_to_domain("plc", "automethod", PlcAutodocDirective) 56 | 57 | app.registry.add_documenter("plc:property", PlcPropertyDocumenter) 58 | app.add_directive_to_domain("plc", "autoproperty", PlcAutodocDirective) 59 | 60 | app.registry.add_documenter("plc:struct", PlcStructDocumenter) 61 | app.registry.add_documenter("plc:member", PlcStructMemberDocumenter) 62 | app.add_directive_to_domain("plc", "autostruct", PlcAutodocDirective) 63 | 64 | app.registry.add_documenter("plc:gvl", PlcVariableListDocumenter) 65 | app.add_directive_to_domain("plc", "autogvl", PlcAutodocDirective) 66 | 67 | app.registry.add_documenter("plc:folder", PlcFolderDocumenter) 68 | app.add_directive_to_domain("plc", "autofolder", PlcAutodocDirective) 69 | 70 | # Insert a resolver for built-in types 71 | app.connect("missing-reference", builtin_resolver, priority=900) 72 | 73 | return { 74 | "version": __version__, 75 | "parallel_read_safe": True, 76 | "parallel_write_safe": True, 77 | } 78 | 79 | 80 | def analyze(app: Sphinx): 81 | """Perform the analysis of PLC source and extract docs. 82 | 83 | The sources to be scoured are listed in the user's ``conf.py``. 84 | 85 | The analysed results need to be available throughout the rest of the extension. To 86 | accomplish that, we just insert a new property into ``app``. 87 | """ 88 | 89 | # Inserting the shared interpreter into an existing object is not the neatest, but 90 | # it's the best way to keep an instance linked to an `app` object. The alternative 91 | # would be the `app.env.temp_data` dict, which is also nasty. 92 | interpreter = PlcInterpreter() 93 | 94 | source_paths = ( 95 | [app.config.plc_sources] 96 | if isinstance(app.config.plc_sources, str) 97 | else app.config.plc_sources 98 | ) 99 | if source_paths: 100 | if not interpreter.parse_source_files(source_paths): 101 | logger.warning("Could not parse all files in `plc_sources` from conf.py") 102 | 103 | project_file = app.config.plc_project 104 | if project_file: 105 | if not interpreter.parse_plc_project(project_file): 106 | logger.warning( 107 | f"Could not parse all files found in project file {project_file}" 108 | ) 109 | 110 | app._interpreter = interpreter 111 | 112 | 113 | def builtin_resolver( 114 | app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Element 115 | ) -> Optional[Element]: 116 | """Do not emit nitpicky warnings for built-in types. 117 | 118 | Strongly based on :func:`python.builtin_resolver`. 119 | """ 120 | # This seems to be the case when using the signature notation, e.g. `func(x: LREAL)` 121 | if node.get("refdomain") not in ("plc"): 122 | return None # We can only deal with the PLC domain 123 | elif node.get("reftype") in ("class", "obj") and node.get("reftarget") == "None": 124 | return contnode 125 | elif node.get("reftype") in ("class", "obj", "type"): 126 | reftarget = node.get("reftarget") 127 | if _builtin_types_re.match(reftarget): 128 | return contnode 129 | 130 | return None 131 | -------------------------------------------------------------------------------- /src/plcdoc/interpreter.py: -------------------------------------------------------------------------------- 1 | """Contains the PLC StructuredText interpreter.""" 2 | 3 | import os 4 | from typing import List, Dict, Optional, Any 5 | from glob import glob 6 | import logging 7 | import xml.etree.ElementTree as ET 8 | from textx import metamodel_from_file, TextXSyntaxError 9 | 10 | PACKAGE_DIR = os.path.dirname(__file__) 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | TextXMetaClass = Any 15 | """Stub for the result of a TextX interpretation. 16 | 17 | The result are nested classes. But there is no useful inheritance to use for type 18 | checking. 19 | """ 20 | 21 | 22 | class PlcInterpreter: 23 | """Class to perform the PLC file parsing. 24 | 25 | It uses TextX with a declaration to parse the files. 26 | 27 | The parsed objects are stored as their raw TextX format, as meta-models 28 | """ 29 | 30 | # Some object types are documented the same 31 | EQUIVALENT_TYPES = { 32 | "function": ["function", "method"], 33 | "functionblock": ["functionblock", "interface"], 34 | } 35 | 36 | # Document types (as XML nodes) that can be processed 37 | XML_TYPES = ["POU", "DUT", "GVL", "Itf"] 38 | 39 | def __init__(self): 40 | self._meta_model = metamodel_from_file( 41 | os.path.join(PACKAGE_DIR, "st_declaration.tx") 42 | ) 43 | 44 | # Library of processed models, keyed by the objtype and then by the name 45 | self._models: Dict[str, Dict[str, "PlcDeclaration"]] = {} 46 | 47 | # List of relative folders with model key,name 48 | self._folders: Dict[str, List["PlcDeclaration"]] = {} 49 | 50 | self._active_file = "" # For better logging of errors 51 | 52 | self._root_folder: Optional[str] = None # For folder references 53 | 54 | def parse_plc_project(self, path: str) -> bool: 55 | """Parse a PLC project. 56 | 57 | The ``*.plcproj`` file is searched for references to source files. 58 | 59 | :returns: True if successful 60 | """ 61 | tree = ET.parse(path) 62 | root = tree.getroot() 63 | 64 | # The items in the project XML are namespaced, so addressing each item by name 65 | # does not work 66 | if not root.tag.endswith("Project"): 67 | return False 68 | 69 | # Find project root 70 | self._root_folder = os.path.dirname(os.path.normpath(path)) 71 | 72 | source_files = [] 73 | 74 | for item_group in root: 75 | if not item_group.tag.endswith("ItemGroup"): 76 | continue 77 | 78 | for item in item_group: 79 | if item.tag.endswith("Compile"): 80 | source_files.append(item.attrib["Include"]) 81 | 82 | # The paths in the PLC Project are relative to the project file itself: 83 | dir_path = os.path.dirname(path) 84 | source_files = [ 85 | os.path.normpath(os.path.join(dir_path, item)) for item in source_files 86 | ] 87 | 88 | if os.path.sep == "/": 89 | # The project will likely contain Windows paths, which can cause issues 90 | # on Linux 91 | source_files = [path.replace("\\", "/") for path in source_files] 92 | 93 | return self.parse_source_files(source_files) 94 | 95 | def parse_source_files(self, paths: List[str]) -> bool: 96 | """Parse a set of source files. 97 | 98 | `glob` is used, so wildcards are allowed. 99 | 100 | :param paths: Source paths to process 101 | """ 102 | result = True 103 | 104 | for path in paths: 105 | source_files = glob(path) 106 | 107 | if not source_files: 108 | logging.warning(f"Could not find file(s) in: {path}") 109 | else: 110 | for source_file in source_files: 111 | if not self._parse_file(source_file): 112 | result = False 113 | 114 | return result 115 | 116 | def _parse_file(self, filepath) -> bool: 117 | """Process a single PLC file. 118 | 119 | :return: True if a file was processed successfully 120 | """ 121 | 122 | tree = ET.parse(filepath) 123 | root = tree.getroot() 124 | 125 | self._active_file = filepath 126 | 127 | if root.tag != "TcPlcObject": 128 | return False 129 | 130 | # Files really only contain a single object per file anyway 131 | for item in root: 132 | plc_item = item.tag # I.e. "POU" 133 | 134 | if plc_item not in self.XML_TYPES: 135 | logger.warning(f"Skipping file with XML tag {plc_item}") 136 | continue 137 | 138 | # Name is repeated inside the declaration, use it from there instead 139 | # name = item.attrib["Name"] 140 | 141 | object_model = self._parse_declaration(item) 142 | if object_model is None: 143 | # Log entry is made inside _parse_declaration() already 144 | continue 145 | 146 | obj = PlcDeclaration(object_model, filepath) 147 | 148 | # Methods are inside their own subtree with a `Declaration` - simply append 149 | # them to the object 150 | for node in item: 151 | if node.tag in ["Declaration", "Implementation"]: 152 | continue 153 | method_model = self._parse_declaration(node) 154 | if method_model is None: 155 | continue 156 | method = PlcDeclaration(method_model, filepath) 157 | obj.add_child(method) 158 | 159 | self._add_model(obj) 160 | 161 | return True 162 | 163 | def _parse_declaration(self, item) -> Optional["TextXMetaClass"]: 164 | declaration_node = item.find("Declaration") 165 | if declaration_node is None: 166 | return None 167 | try: 168 | meta_model = self._meta_model.model_from_str(declaration_node.text) 169 | return meta_model 170 | except TextXSyntaxError as err: 171 | name = item.attrib.get("Name", "") 172 | logger.error( 173 | "Error parsing node `%s` in file `%s`\n(%s)", 174 | name, 175 | self._active_file, 176 | str(err), 177 | ) 178 | 179 | return None 180 | 181 | def reduce_type(self, key: str): 182 | """If key is one of multiple, return the main type. 183 | 184 | E.g. "method" will be reduced to "function". 185 | """ 186 | for major_type, equivalents in self.EQUIVALENT_TYPES.items(): 187 | if key in equivalents: 188 | return major_type 189 | 190 | return key 191 | 192 | def _add_model( 193 | self, obj: "PlcDeclaration", parent: Optional["PlcDeclaration"] = None 194 | ): 195 | """Get processed model and add it to our library. 196 | 197 | Also store references to the children of an object directly, so they can be 198 | found easily later. 199 | 200 | :param obj: Processed model 201 | :param parent: Object that this object belongs to 202 | """ 203 | key = self.reduce_type(obj.objtype) 204 | 205 | name = (parent.name + "." if parent else "") + obj.name 206 | 207 | if key not in self._models: 208 | self._models[key] = {} 209 | 210 | self._models[key][name] = obj 211 | 212 | if not parent: 213 | for child in obj.children.values(): 214 | self._add_model(child, obj) 215 | 216 | # Build a lookup of the folders (but skip child items!) 217 | if self._root_folder and obj.file.startswith(self._root_folder): 218 | file_relative = obj.file[len(self._root_folder) :] # Remove common path 219 | folder = os.path.dirname(file_relative).lstrip(os.sep) 220 | if folder not in self._folders: 221 | self._folders[folder] = [] 222 | self._folders[folder].append(obj) 223 | 224 | def get_object(self, name: str, objtype: Optional[str] = None) -> "PlcDeclaration": 225 | """Search for an object by name in parsed models. 226 | 227 | If ``objtype`` is `None`, any object is returned. 228 | 229 | :param name: Object name 230 | :param objtype: objtype of the object to look for ("function", etc.) 231 | :raises: KeyError if the object could not be found 232 | """ 233 | if objtype: 234 | objtype = self.reduce_type(objtype) 235 | try: 236 | return self._models[objtype][name] 237 | except KeyError: 238 | pass 239 | 240 | else: 241 | for models_set in self._models.values(): 242 | if name in models_set: 243 | return models_set[name] 244 | 245 | raise KeyError(f"Failed to find object `{name}` for the type `{objtype}`") 246 | 247 | def get_objects_in_folder(self, folder: str) -> List["PlcDeclaration"]: 248 | """Search for objects inside a folder. 249 | 250 | Currently no recursion is possible! 251 | 252 | :param folder: Folder name 253 | """ 254 | if folder in self._folders: 255 | return self._folders[folder] 256 | 257 | raise KeyError(f"Found no models in the folder `{folder}`") 258 | 259 | 260 | class PlcDeclaration: 261 | """Wrapper class for the result of the TextX parsing of a PLC source file. 262 | 263 | Instances of these declarations are stored in an interpreter object. 264 | 265 | An object also stores a list of all child objects (e.g. methods). 266 | 267 | The `objtype` is as they appear in :class:`StructuredTextDomain`. 268 | """ 269 | 270 | def __init__(self, meta_model: TextXMetaClass, file=None): 271 | """ 272 | 273 | :param meta_model: Parsing result 274 | :param file: Path to the file this model originates from 275 | """ 276 | self._objtype = None 277 | self._name = None 278 | 279 | if meta_model.functions: 280 | self._model = meta_model.functions[0] 281 | self._objtype = self._model.function_type.lower().replace("_", "") 282 | 283 | if meta_model.types: 284 | self._model = meta_model.types[0] 285 | type_str = type(self._model.type).__name__ 286 | if "Enum" in type_str: 287 | self._objtype = "enum" 288 | elif "Struct" in type_str: 289 | self._objtype = "struct" 290 | elif "Union" in type_str: 291 | self._objtype = "union" 292 | elif "Alias" in type_str: 293 | self._objtype = "alias" 294 | else: 295 | raise ValueError(f"Could not categorize type `{type_str}`") 296 | 297 | if meta_model.properties: 298 | self._model = meta_model.properties[0] 299 | self._objtype = "property" 300 | 301 | if meta_model.variable_lists: 302 | if file is None: 303 | raise ValueError( 304 | "Cannot parse GVL without file as no naming is present" 305 | ) 306 | self._name, _ = os.path.splitext(os.path.basename(file)) 307 | # GVL are annoying because no naming is present in source - we need to 308 | # extract it from the file name 309 | 310 | self._model = meta_model 311 | # ^ A GVL can contain multiple lists, store them all 312 | self._objtype = "gvl" 313 | 314 | if self._objtype is None: 315 | raise ValueError(f"Unrecognized declaration in `{meta_model}`") 316 | 317 | if self._name is None: 318 | self._name = self._model.name 319 | self._file: Optional[str] = file 320 | self._children: Dict[str, "PlcDeclaration"] = {} 321 | 322 | def __repr__(self): 323 | type_ = type(self) 324 | return ( 325 | f"<{type_.__module__}.{type_.__qualname__} object, name `{self.name}`, " 326 | f"at {hex(id(self))}>" 327 | ) 328 | 329 | @property 330 | def name(self) -> str: 331 | return self._name 332 | 333 | @property 334 | def objtype(self) -> str: 335 | return self._objtype 336 | 337 | @property 338 | def file(self) -> str: 339 | return self._file or "" 340 | 341 | @property 342 | def children(self) -> Dict[str, "PlcDeclaration"]: 343 | return self._children 344 | 345 | @property 346 | def members(self) -> List[TextXMetaClass]: 347 | if not self._model.type: 348 | return [] 349 | return self._model.type.members 350 | 351 | def get_comment(self) -> Optional[str]: 352 | """Process main block comment from model into a neat list. 353 | 354 | A list is created for each 'region' of comments. The first comment block above 355 | a declaration is the most common one. 356 | """ 357 | if hasattr(self._model, "comment") and self._model.comment is not None: 358 | # Probably a comment line 359 | big_block: str = self._model.comment.text 360 | elif hasattr(self._model, "comments") and self._model.comments: 361 | # Probably a comment block (amongst multiple maybe) 362 | block_comment = None 363 | for comment in reversed(self._model.comments): 364 | # Find last block-comment 365 | if type(comment).__name__ == "CommentBlock": 366 | block_comment = comment 367 | break 368 | 369 | if block_comment is None: 370 | return None 371 | 372 | big_block: str = block_comment.text 373 | else: 374 | return None 375 | 376 | big_block = big_block.strip() # Get rid of whitespace 377 | 378 | # Remove comment indicators (cannot get rid of them by TextX) 379 | if big_block.startswith("(*"): 380 | big_block = big_block[2:] 381 | if big_block.endswith("*)"): 382 | big_block = big_block[:-2] 383 | 384 | # It looks like Windows line endings are already lost by now, but make sure 385 | big_block = big_block.replace("\r\n", "\n") 386 | 387 | return big_block 388 | 389 | def get_args(self, skip_internal=True) -> List: 390 | """Return arguments. 391 | 392 | :param skip_internal: If true, only return in, out and inout variables 393 | :retval: Empty list if there are none or arguments are applicable to this type. 394 | """ 395 | args = [] 396 | 397 | if hasattr(self._model, "lists"): 398 | for var_list in self._model.lists: 399 | var_kind = var_list.name.lower() 400 | if skip_internal and var_kind not in [ 401 | "var_input", 402 | "var_output", 403 | "var_input_output", 404 | ]: 405 | continue # Skip internal variables `VAR` 406 | 407 | for var in var_list.variables: 408 | var.kind = var_kind 409 | args.append(var) 410 | 411 | if hasattr(self._model, "variables"): 412 | for var in self._model.variables: 413 | var.kind = "var" 414 | args.append(var) 415 | 416 | if hasattr(self._model, "variable_lists"): 417 | for var_list in self._model.variable_lists: 418 | for var in var_list.variables: 419 | var.kind = "var" 420 | args.append(var) 421 | 422 | return args 423 | 424 | def add_child(self, child: "PlcDeclaration"): 425 | self._children[child.name] = child 426 | -------------------------------------------------------------------------------- /src/plcdoc/roles.py: -------------------------------------------------------------------------------- 1 | """Contains Spinx-referencable objects.""" 2 | 3 | from sphinx.roles import XRefRole 4 | 5 | 6 | class PlcXRefRole(XRefRole): 7 | """Cross-reference role for PLC domain.""" 8 | 9 | pass 10 | -------------------------------------------------------------------------------- /src/plcdoc/st_declaration.tx: -------------------------------------------------------------------------------- 1 | /* 2 | Parse the StructuredText declaration specifically. 3 | 4 | In practice all segments are in different XML leaves, but the syntax would allow for combinations. We pretend everything 5 | could be in a big file. 6 | 7 | Note: `FUNCTION*` does not have a closing call for some reason! 8 | 9 | There are a lot of `comments*=CommentAny`. This will offer a list of comments, the relevant docblocks need to be 10 | extracted later. There seems no better way to do this in TextX. 11 | Comment captures should be moved down (= more basic elements) as much as possible to limit their usage. 12 | 13 | There can be dynamic expressions in variable declarations, which are very tricky to parse. Therefore expected 14 | expressions are parsed greedily as whole strings. As a consequence, blocks like argument lists will result in a long 15 | string including the parentheses and commas. 16 | */ 17 | 18 | Declaration: 19 | types*=TypeDef 20 | properties*=Property 21 | functions*=Function 22 | variable_lists*=VariableList 23 | CommentAny* 24 | ; 25 | 26 | /* 27 | --------------------------------------------------- 28 | */ 29 | /* 30 | One instance of an ENUM or STRUCTURE 31 | */ 32 | TypeDef: 33 | comments*=CommentAny 34 | 'TYPE' 35 | name=ID 36 | ('EXTENDS' extends=Fqn)? 37 | ':' 38 | CommentAny* 39 | type=AnyType 40 | CommentAny* 41 | 'END_TYPE' 42 | ; 43 | 44 | AnyType: 45 | TypeStruct | TypeUnion | TypeEnum | TypeAlias 46 | ; 47 | 48 | TypeStruct: 49 | 'STRUCT' 50 | members*=Variable 51 | CommentAny* 52 | // Catch trailing comments here, not at the end of `Variable` 53 | 'END_STRUCT' 54 | ; 55 | 56 | TypeUnion: 57 | 'UNION' 58 | members*=Variable 59 | CommentAny* 60 | // Catch trailing comments here, not at the end of `Variable` 61 | 'END_UNION' 62 | ; 63 | 64 | TypeEnum: 65 | '(' 66 | values*=EnumOption 67 | CommentAny* 68 | ')' 69 | (base_type=Fqn)? 70 | (default=EnumDefault)? 71 | SemiColon 72 | ; 73 | 74 | EnumOption: 75 | CommentAny* 76 | name=ID 77 | (':=' number=INT)? 78 | (',')? 79 | (comment=CommentLine)? 80 | ; 81 | 82 | EnumDefault: 83 | ':=' 84 | option=ID 85 | // Enum default must be a literal field, it cannot be e.g. an integer 86 | ; 87 | 88 | TypeAlias: 89 | base=VariableType 90 | CommentAny* 91 | // Catch trailing comments here, not at the end of `Variable` 92 | SemiColon 93 | ; 94 | 95 | /* 96 | --------------------------------------------------- 97 | */ 98 | /* 99 | One instance of a FUNCTION, FUNCTION_BLOCK, METHOD, ... 100 | */ 101 | Function: 102 | comments*=CommentAny 103 | function_type=FunctionType 104 | (abstract?='ABSTRACT' final?='FINAL' (visibility=Visibility)?)# 105 | name=ID 106 | ('EXTENDS' extends=Fqn)? 107 | ('IMPLEMENTS' implements=Fqn)? 108 | (':' return=VariableType (arglist=ArgList)?)? 109 | (SemiColon)? 110 | lists*=VariableList 111 | ; 112 | 113 | FunctionType: 114 | 'FUNCTION_BLOCK' | 'FUNCTION' | 'INTERFACE' | 'METHOD' | 'PROGRAM' 115 | ; 116 | 117 | Visibility: 118 | 'PUBLIC' | 'PRIVATE' | 'PROTECTED' | 'INTERNAL' 119 | ; 120 | 121 | /* 122 | --------------------------------------------------- 123 | */ 124 | 125 | Property: 126 | comments*=CommentAny 127 | 'PROPERTY' 128 | (visibility=Visibility)? 129 | name=ID 130 | ':' 131 | type=VariableType 132 | ; 133 | 134 | /* 135 | --------------------------------------------------- 136 | */ 137 | /* 138 | Variable declarations. There are many different notations, so this is tricky. 139 | */ 140 | 141 | VariableList: 142 | CommentAny* 143 | name=VariableListType 144 | (constant?='CONSTANT')? 145 | (persistent?='PERSISTENT')? 146 | variables*=Variable 147 | CommentAny* 148 | // Catch trailing comments here, not at the end of `Variable` 149 | 'END_VAR' 150 | ; 151 | 152 | VariableListType: 153 | /VAR_\w+/ | 'VAR' 154 | ; 155 | 156 | /* 157 | Single variable declaration 158 | 159 | Unfortunately, it is possible to define multiple variables inline - those are ignored for now 160 | */ 161 | Variable: 162 | CommentAny* 163 | name=ID 164 | (',' ID)* 165 | (address=Address)? 166 | ':' 167 | type=VariableType 168 | (arglist=ArgList)? 169 | (AssignmentSymbol value=AssignmentValue)? 170 | SemiColon 171 | comment=CommentLine? 172 | ; 173 | 174 | AssignmentSymbol: 175 | (':=') | ('REF=') 176 | ; 177 | 178 | VariableType: 179 | (array=VariableTypeArray)? 180 | (pointer=PointerLike 'TO')? 181 | name=BaseType 182 | ; 183 | 184 | /* 185 | Specifically the string might also have a dimension attribute 186 | */ 187 | BaseType: 188 | StringType | Fqn 189 | ; 190 | 191 | /* 192 | Strings are very annoying because they have the size arg list but also assignment values. 193 | The very broad wildcard will then also match the assignment. So instead catch it specifically. 194 | */ 195 | StringType: 196 | 'STRING' 197 | ( ( '(' (NUMBER | Fqn) ')' ) | ( '[' (NUMBER | Fqn) ']' ) ) 198 | ; 199 | 200 | PointerLike: 201 | 'POINTER' | 'REFERENCE' 202 | ; 203 | 204 | VariableTypeArray: 205 | 'ARRAY'- ArrayRange 'OF'- 206 | ; 207 | 208 | ArrayRange: 209 | '['- 210 | /[^\]]+/ 211 | ']'- 212 | // Match anything except the square bracket at the end 213 | ; 214 | 215 | AssignmentValue: 216 | ArgList | Expression 217 | ; 218 | 219 | ArgList: 220 | ( '(' | '[' ) 221 | /[^;]*/ 222 | // Match anything, including parentheses, up to (but excluding) the semicolon 223 | ; 224 | 225 | Address: 226 | 'AT' '%' /[A-Z%\.\*]/+ 227 | ; 228 | 229 | /* 230 | Any variable name basically (do not skip whitespace because we don't want to match "first. second") 231 | */ 232 | Fqn[noskipws]: 233 | /\s/*- 234 | ID('.'ID)* 235 | /\s/*- 236 | ; 237 | 238 | /* 239 | Semi-colons may be repeated in valid code 240 | */ 241 | SemiColon: 242 | ';'+ 243 | ; 244 | 245 | /* 246 | Anything that is considered a value: a literal, a variable, or e.g. a sum 247 | */ 248 | Expression: 249 | ExpressionString | ExpressionAnything 250 | ; 251 | 252 | /* 253 | Because a string expression could use a syntax character, we need to make an effort match string content, to 254 | escape the content. 255 | 256 | We use a literal string match, instead of TextX's `STRING`, because we need to keep the quotes so we can later 257 | still distinguish a literal string type (over e.g. a variable name). 258 | */ 259 | ExpressionString: 260 | /'.*'/ 261 | ; 262 | 263 | ExpressionAnything: 264 | /[^;]*/ 265 | // Match anything, including parentheses, up to (but excluding) the semicolon 266 | ; 267 | 268 | /* 269 | --------------------------------------------------- 270 | */ 271 | 272 | /* 273 | The `Comment` clause is reserved, which removes comments but then we cannot process them. 274 | 275 | We also put `Attribute` in the comment pile, because it can be placed just about everywhere 276 | 277 | A named field is added for comments so we can easily distinguish between comment lines and blocks. 278 | */ 279 | 280 | CommentAny: 281 | CommentLine | CommentBlock | Attribute 282 | ; 283 | 284 | CommentLine: 285 | '//'- text=/.*$/ 286 | ; 287 | 288 | CommentBlock[noskipws]: 289 | /\s*/- text=/\(\*(.|\n)*?\*\)/ /\s*/- 290 | // Use the non-greedy repetition `*?` 291 | ; 292 | 293 | /* 294 | The attribute comment is nasty because there are basically no rules - hence just do a wildcard match 295 | */ 296 | Attribute: 297 | '{' 298 | field=ID 299 | (name=STRING)? 300 | (content=/[^}]+/)? 301 | '}' 302 | ; 303 | 304 | //Comment: 305 | // CommentAny 306 | //; 307 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEMCON/plcdoc/f6cd6881cd93e853502f183de8054e54ac008148/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for these tests. 3 | """ 4 | 5 | import os 6 | import shutil 7 | 8 | import docutils 9 | import pytest 10 | 11 | import sphinx 12 | from sphinx.testing import comparer 13 | from sphinx.testing.path import path 14 | 15 | pytest_plugins = "sphinx.testing.fixtures" 16 | 17 | # Exclude 'roots' dirs for pytest test collector 18 | collect_ignore = ["roots"] 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | def rootdir(): 23 | return path(__file__).parent.abspath() / "roots" 24 | 25 | 26 | def pytest_report_header(config): 27 | header = "libraries: Sphinx-%s, docutils-%s" % ( 28 | sphinx.__display_version__, 29 | docutils.__version__, 30 | ) 31 | if hasattr(config, "_tmp_path_factory"): 32 | header += "\nbase tempdir: %s" % config._tmp_path_factory.getbasetemp() 33 | 34 | return header 35 | 36 | 37 | def pytest_assertrepr_compare(op, left, right): 38 | comparer.pytest_assertrepr_compare(op, left, right) 39 | 40 | 41 | def _initialize_test_directory(session): 42 | if "SPHINX_TEST_TEMPDIR" in os.environ: 43 | tempdir = os.path.abspath(os.getenv("SPHINX_TEST_TEMPDIR")) 44 | print("Temporary files will be placed in %s." % tempdir) 45 | 46 | if os.path.exists(tempdir): 47 | shutil.rmtree(tempdir) 48 | 49 | os.makedirs(tempdir) 50 | 51 | 52 | def pytest_sessionstart(session): 53 | _initialize_test_directory(session) 54 | -------------------------------------------------------------------------------- /tests/plc_code/E_Filter.txt: -------------------------------------------------------------------------------- 1 | (* 2 | Qualified and strict are both important. 3 | *) 4 | {attribute 'qualified_only'} 5 | {attribute 'strict'} 6 | (* 7 | Different optical filters. 8 | *) 9 | TYPE E_Filter : 10 | ( 11 | NoFilter := 1, 12 | // Pointless comment 13 | Filter434nm := 2, 14 | Filter697nm := 6 // Note: non-consecutive number! 15 | ) USINT; // Specify base type so API is explicit 16 | END_TYPE 17 | 18 | TYPE E_FilterDefault : 19 | ( 20 | NoFilter := 0, 21 | Filter434nm, 22 | Filter697nm 23 | ) USINT := NoFilter; 24 | END_TYPE 25 | -------------------------------------------------------------------------------- /tests/plc_code/E_Options.txt: -------------------------------------------------------------------------------- 1 | TYPE E_Options : 2 | ( 3 | Default := 0, 4 | Option1, 5 | Option2 6 | ); 7 | END_TYPE 8 | 9 | TYPE E_OptionsDefault : 10 | ( 11 | Default := 0, 12 | Option1, 13 | Option2 14 | ) := DEFAULT; 15 | END_TYPE 16 | -------------------------------------------------------------------------------- /tests/plc_code/FB_Comments.txt: -------------------------------------------------------------------------------- 1 | // Ignored 2 | {attribute 'naming' := 'on'} 3 | (* 4 | Ignored 5 | *) 6 | (* 7 | Important 8 | *) 9 | FUNCTION_BLOCK FB_MyBlock EXTENDS FB_MyBlock // Ignored 10 | // Ignored 11 | VAR_INPUT // Ignored 12 | someInput : LREAL; // Important 13 | // Ignored 14 | otherInput : BOOL; // Important 15 | END_VAR 16 | // Ignored 17 | VAR_OUTPUT 18 | {attribute 'stuff' := ' 19 | hello: how do you do 20 | again: what's up 21 | '} 22 | myOutput : UINT; // Important 23 | END_VAR 24 | VAR CONSTANT 25 | secondClause : LREAL; //Important 26 | // Ignored 27 | END_VAR 28 | VAR 29 | {analysis -33} 30 | some_string:STRING:='hoi';//Important 31 | internal_variable : BOOL := FALSE; 32 | END_VAR 33 | // Ignored 34 | // Ignored 35 | // Ignored 36 | {attribute 'naming' := 'on'} 37 | (* 38 | Ignored 39 | *) 40 | // Ignored 41 | 42 | (* 43 | Ignored 44 | *) 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/plc_code/FB_MyBlock.txt: -------------------------------------------------------------------------------- 1 | (* 2 | My doc block! 3 | And a second line 4 | 5 | How far does this go? 6 | 7 | Lol Lol 8 | *) 9 | FUNCTION_BLOCK FB_MyBlock 10 | VAR_INPUT 11 | someInput : LREAL; // Some comment! 12 | otherInput : BOOL; 13 | END_VAR 14 | VAR_OUTPUT 15 | myOutput : UINT; 16 | END_VAR 17 | VAR_INPUT 18 | // Useless comment 19 | secondClause : LREAL; //Thisisalsoacomment 20 | // Useless comment 21 | END_VAR 22 | VAR 23 | some_string:STRING:='hoi'; 24 | internal_variable : BOOL := FALSE; 25 | END_VAR 26 | // Useless comment 27 | // Useless comment 28 | // Useless comment 29 | // Useless comment 30 | -------------------------------------------------------------------------------- /tests/plc_code/FB_MyBlockExtended.txt: -------------------------------------------------------------------------------- 1 | FUNCTION_BLOCK FB_MyBlockExtended EXTENDS FB_MyBlock 2 | VAR_INPUT 3 | END_VAR 4 | VAR_OUTPUT 5 | END_VAR 6 | VAR 7 | END_VAR 8 | -------------------------------------------------------------------------------- /tests/plc_code/FB_Variables.txt: -------------------------------------------------------------------------------- 1 | FUNCTION_BLOCK FB_WithAllKindsOfVariableConstructors 2 | VAR 3 | myfloat_no_ws:REAL; 4 | myfloat : REAL ; 5 | 6 | mydoubleinit1 : LREAL:=1.0; 7 | mydoubleinit2 : LREAL := 1.0; 8 | myinteger : SINT := 420; 9 | mystring : STRING := 'test'; 10 | 11 | int_arth_plus : INT := 1 + 1; 12 | int_arth_minus : INT := 10-1; 13 | int_arth_parent : INT := 1 + (LIM + 1) * 5; 14 | 15 | my_object : MyObject(); 16 | my_object1 : MyObject(7); 17 | my_object2 : MyObject('hi', 23, FALSE); 18 | my_object3 : MyObject(text := 'hi', number := 23, flag := FALSE); 19 | my_object4 : MyObject( 20 | text := 'hi', 21 | number := 23, 22 | flag := FALSE 23 | ); 24 | mystring_size1 : STRING(15); 25 | mystring_size2 : STRING[17]; 26 | mystring_size3 : STRING(Module.SIZE); 27 | mystring_size4 : STRING[SIZE]; 28 | mystring_size5 : STRING(35) := 'Unknown'; 29 | mystring_size6 : STRING[Module.SIZE] := 'Unknown'; 30 | 31 | mystring_escape : STRING := ':;"'; 32 | 33 | mystring_concat : STRING := CONCAT('abc', 'xyz'); 34 | 35 | myint : INT := SomeConstant; 36 | myint2 : INT := E_Error.NoError; 37 | 38 | mylist : ARRAY[0..4]OF BOOL; 39 | mylist_ws : ARRAY [ 0 .. 4] OF BOOL; 40 | mylist_var_idx : ARRAY [Idx.start..Idx.end] OF BOOL; 41 | mylist_var_sum : ARRAY [0 .. MAX-1] OF BOOL; 42 | mylist_multi : ARRAY [1..10,1..10] OF BOOL; 43 | mylist_multi2 : ARRAY [1 .. 10 , 1 .. 10] OF BOOL; 44 | mylist_dyn : ARRAY [*] OF BOOL; 45 | mylist_dyn_multi : ARRAY [*,*,*] OF BOOL; 46 | 47 | mystruct : MyStruct(); 48 | mystruct2 : MyStruct := (number := 1.0, text := 'hi'); 49 | 50 | specialint1 : UDINT := 2#1001_0110; 51 | specialint2 : UDINT := 8#67; 52 | specialint3 : UDINT := 16#FF_FF_FF; 53 | specialint4 : UDINT := UDINT#16#1; 54 | specialint5 : UDINT := 1_000_000; 55 | specialint6 : USINT := (1..Module.MAX); 56 | 57 | mypointer1 : POINTER TO UDINT; 58 | mypointer2 : REFERENCE TO UDINT; 59 | mypointer3 : REFERENCE TO FB_Motor REF= _motor; 60 | 61 | extra_semicolons : INT := 7;;;;;;;;;; 62 | 63 | timeout1 : TIME := T#2S; 64 | timeout2 : TIME := T#12m13s14ms; 65 | 66 | inline1, inlin2, inline3 : INT; 67 | 68 | END_VAR 69 | -------------------------------------------------------------------------------- /tests/plc_code/GlobalVariableList.txt: -------------------------------------------------------------------------------- 1 | {attribute 'qualified_only'} 2 | (* 3 | List of global variables. 4 | *) 5 | VAR_GLOBAL 6 | someGlobal : LREAL; // Entry 1 7 | otherGlobal : INT; // Entry 2 8 | END_VAR 9 | -------------------------------------------------------------------------------- /tests/plc_code/Main.txt: -------------------------------------------------------------------------------- 1 | PROGRAM MAIN 2 | VAR 3 | request : UDINT := 0; 4 | ack : UDINT := 0; 5 | 6 | block : FB_MyBlock; 7 | 8 | result : LREAL; 9 | 10 | END_VAR 11 | -------------------------------------------------------------------------------- /tests/plc_code/MyStructure.txt: -------------------------------------------------------------------------------- 1 | {attribute 'pack_mode' := '8'} 2 | TYPE MyStructure : 3 | STRUCT 4 | member1 : LREAL; // Some member 5 | member2 : LREAL; // Some member 6 | END_STRUCT 7 | END_TYPE -------------------------------------------------------------------------------- /tests/plc_code/MyStructureExtended.txt: -------------------------------------------------------------------------------- 1 | TYPE MyStructureExtended EXTENDS MyStructure : 2 | STRUCT 3 | member2 : UDINT; // Another property 4 | anArray : ARRAY[0..10] OF BOOL; 5 | END_STRUCT 6 | END_TYPE -------------------------------------------------------------------------------- /tests/plc_code/Properties.txt: -------------------------------------------------------------------------------- 1 | PROPERTY PUBLIC MyProperty : BOOL 2 | -------------------------------------------------------------------------------- /tests/plc_code/RegularFunction.txt: -------------------------------------------------------------------------------- 1 | (* 2 | Wanna hear something cool? 3 | 4 | Listen up. 5 | *) 6 | FUNCTION RegularFunction : LREAL 7 | VAR_INPUT 8 | input : LREAL; // Herpaderp 9 | END_VAR 10 | VAR 11 | END_VAR 12 | -------------------------------------------------------------------------------- /tests/plc_code/T_ALIAS.txt: -------------------------------------------------------------------------------- 1 | TYPE T_INTERLOCK : WORD; END_TYPE 2 | 3 | TYPE T_Message : STRING[50]; 4 | END_TYPE 5 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/.gitignore: -------------------------------------------------------------------------------- 1 | # IDE stuff 2 | *.bak 3 | .vs 4 | *.suo 5 | *.project.~u 6 | *.user 7 | *.dbg 8 | 9 | # TwinCAT files 10 | *.tmc 11 | TrialLicense.tclrs 12 | _CompileInfo/ 13 | _ModuleInstall/ 14 | _Boot/ 15 | _Libraries/ 16 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/DUTs/E_Options.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/DUTs/MyStructure.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/DUTs/MyStructureExtended.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/GVLs/GVL_Main.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/MyPLC.plcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0.0 4 | 2.0 5 | {72131fae-9f77-4f70-b1d4-eed067827eae} 6 | True 7 | true 8 | true 9 | false 10 | MyPLC 11 | 3.1.4023.0 12 | {92516cdb-d5f4-4c02-aefd-435f9f8ed07d} 13 | {513257d4-bb0a-4cfa-ba6f-771697ca9037} 14 | {34f533c3-78fc-4a75-984b-ce492600c614} 15 | {d36b44fd-c72a-4179-b5ce-77ef9c54712c} 16 | {9659c42b-9e58-44d8-89cf-2dac5a20ce19} 17 | {b12a38fe-e83a-4fc7-8c90-3a12a586acda} 18 | 19 | 20 | 21 | Code 22 | 23 | 24 | Code 25 | 26 | 27 | Code 28 | 29 | 30 | Code 31 | true 32 | 33 | 34 | Code 35 | 36 | 37 | Code 38 | 39 | 40 | Code 41 | 42 | 43 | Code 44 | 45 | 46 | Code 47 | 48 | 49 | Code 50 | 51 | 52 | Code 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Tc2_Standard, * (Beckhoff Automation GmbH) 64 | Tc2_Standard 65 | 66 | 67 | Tc2_System, * (Beckhoff Automation GmbH) 68 | Tc2_System 69 | 70 | 71 | Tc3_Module, * (Beckhoff Automation GmbH) 72 | Tc3_Module 73 | 74 | 75 | 76 | 77 | Content 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | "<ProjectRoot>" 86 | 87 | 88 | 89 | 90 | 91 | System.Collections.Hashtable 92 | {54dd0eac-a6d8-46f2-8c27-2f43c7e49861} 93 | System.String 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/FB_MyBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 21 | 22 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/FB_SecondBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/MAIN.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 15 | 16 | request THEN 17 | ack := request; 18 | END_IF 19 | 20 | result := RegularFunction(input := 3.14, other_arg := 7); 21 | ]]> 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/PlainFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/PlainFunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/POUs/RegularFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/MyPLC/PlcTask.TcTTO: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10000 6 | 20 7 | 8 | MAIN 9 | 10 | {ea33fa62-5573-4d24-bc0b-e8b02def7924} 11 | {5e56e7c6-f600-4ec7-bbcc-a029327e1e11} 12 | {4665a91e-8845-4636-b731-315bc647ea6c} 13 | {0599693d-3f2b-4896-b64c-b066dcca0e73} 14 | {e1bcb4cf-0070-49b9-9f18-071b4ce751c7} 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/TwinCAT Solution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32929.386 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{B1E792BE-AA5F-4E3C-8C82-674BF9C0715B}") = "TwinCAT Solution", "TwinCAT Solution.tsproj", "{F4E72638-7AAA-400F-A722-C9DE6F5B1048}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|TwinCAT CE7 (ARMV7) = Debug|TwinCAT CE7 (ARMV7) 11 | Debug|TwinCAT OS (ARMT2) = Debug|TwinCAT OS (ARMT2) 12 | Debug|TwinCAT RT (x64) = Debug|TwinCAT RT (x64) 13 | Debug|TwinCAT RT (x86) = Debug|TwinCAT RT (x86) 14 | Release|TwinCAT CE7 (ARMV7) = Release|TwinCAT CE7 (ARMV7) 15 | Release|TwinCAT OS (ARMT2) = Release|TwinCAT OS (ARMT2) 16 | Release|TwinCAT RT (x64) = Release|TwinCAT RT (x64) 17 | Release|TwinCAT RT (x86) = Release|TwinCAT RT (x86) 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7) 21 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7) 22 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2) 23 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2) 24 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64) 25 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64) 26 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86) 27 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86) 28 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7) 29 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7) 30 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2) 31 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2) 32 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64) 33 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64) 34 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86) 35 | {F4E72638-7AAA-400F-A722-C9DE6F5B1048}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86) 36 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT CE7 (ARMV7).ActiveCfg = Debug|TwinCAT CE7 (ARMV7) 37 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT CE7 (ARMV7).Build.0 = Debug|TwinCAT CE7 (ARMV7) 38 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT OS (ARMT2).ActiveCfg = Debug|TwinCAT OS (ARMT2) 39 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT OS (ARMT2).Build.0 = Debug|TwinCAT OS (ARMT2) 40 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT RT (x64).ActiveCfg = Debug|TwinCAT RT (x64) 41 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT RT (x64).Build.0 = Debug|TwinCAT RT (x64) 42 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT RT (x86).ActiveCfg = Debug|TwinCAT RT (x86) 43 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Debug|TwinCAT RT (x86).Build.0 = Debug|TwinCAT RT (x86) 44 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT CE7 (ARMV7).ActiveCfg = Release|TwinCAT CE7 (ARMV7) 45 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT CE7 (ARMV7).Build.0 = Release|TwinCAT CE7 (ARMV7) 46 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT OS (ARMT2).ActiveCfg = Release|TwinCAT OS (ARMT2) 47 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT OS (ARMT2).Build.0 = Release|TwinCAT OS (ARMT2) 48 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT RT (x64).ActiveCfg = Release|TwinCAT RT (x64) 49 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT RT (x64).Build.0 = Release|TwinCAT RT (x64) 50 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT RT (x86).ActiveCfg = Release|TwinCAT RT (x86) 51 | {72131FAE-9F77-4F70-B1D4-EED067827EAE}.Release|TwinCAT RT (x86).Build.0 = Release|TwinCAT RT (x86) 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {55D9E66A-30EE-4D42-9A84-C5FE5999DE60} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /tests/plc_code/TwinCAT PLC/TwinCAT Solution.tsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PlcTask 8 | 9 | 10 | 11 | 12 | 13 | 14 | MyPLC Instance 15 | {08500001-0000-0000-F000-000000000064} 16 | 17 | 18 | 0 19 | PlcTask 20 | 21 | #x02010030 22 | 23 | 20 24 | 10000000 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/plc_code/Unions.txt: -------------------------------------------------------------------------------- 1 | TYPE U_MsgMetaData : 2 | UNION 3 | a : LREAL; 4 | b : LINTT; 5 | c : WORD; 6 | END_UNION 7 | END_TYPE 8 | -------------------------------------------------------------------------------- /tests/roots/test-domain-plc/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(".")) 5 | 6 | extensions = ["plcdoc"] 7 | 8 | # The suffix of source filenames. 9 | source_suffix = ".rst" 10 | 11 | nitpicky = True 12 | -------------------------------------------------------------------------------- /tests/roots/test-domain-plc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Functions ----------------------------- 3 | 4 | .. plc:function:: BoringFunction(y: LREAL, x: LREAL) : LREAL 5 | 6 | Get the 4-quadrant arctan of a coordinates. 7 | 8 | :param y: Y-coordinate 9 | :param x: X-coordinate 10 | :returns: Angle in radians 11 | 12 | 13 | .. Function Blocks ----------------------------- 14 | 15 | .. plc:functionblock:: FB_TypedByHand(SomeInput, OtherInput, Buffer, IsReady, HasError) 16 | 17 | This is a very cool function block! 18 | 19 | :var_in LREAL SomeInput: Description for SomeInput. 20 | :IN BOOL OtherInput: About OtherInput. 21 | :IN_OUT Buffer: 22 | :var_in_out_type Buffer: LREAL 23 | :OUT BOOL IsReady: Whether it is ready. 24 | :OUT BOOL HasError: 25 | 26 | .. plc:functionblock:: FunctionBlockWithMethod 27 | 28 | .. plc:method:: SomeMethod() 29 | 30 | .. plc:method:: FunctionBlockWithMethod.MethodWithPrefix() 31 | 32 | .. plc:method:: FunctionBlockWithMethod.SomeMethodStandAlone() 33 | 34 | 35 | .. Enums ----------------------------- 36 | 37 | .. plc:enum:: E_Options 38 | 39 | I am options 40 | 41 | .. plc:enum:: Orientation 42 | 43 | .. plc:enumerator:: \ 44 | FaceUp 45 | FaceDown 46 | 47 | I am an orientation. 48 | 49 | 50 | .. Structs ----------------------------- 51 | 52 | .. plc:struct:: ST_MyStruct 53 | 54 | I have properties! 55 | 56 | .. plc:struct:: ST_MyStruct2 57 | 58 | .. plc:property:: \ 59 | FaceUp 60 | FaceDown 61 | 62 | 63 | .. GVL ----------------------------- 64 | 65 | .. plc:gvl:: GVL_MyList 66 | 67 | :var LREAL my_double: Some double-type variable 68 | :var USINT some_int: My short integer 69 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(".")) 5 | 6 | extensions = ["plcdoc"] 7 | 8 | # The suffix of source filenames. 9 | source_suffix = ".rst" 10 | 11 | nitpicky = True 12 | 13 | plc_sources = [ 14 | os.path.join(os.path.abspath("."), "src_plc", item) 15 | for item in ["*.TcPOU", "*.TcDUT", "*.TcGVL"] 16 | ] 17 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Functions ----------------------------- 3 | 4 | .. plc:autofunction:: RegularFunction 5 | 6 | .. plc:autofunction:: PlainFunction 7 | 8 | 9 | .. Function Blocks ----------------------------- 10 | 11 | .. plc:autofunctionblock:: PlainFunctionBlock 12 | 13 | .. plc:autofunctionblock:: FB_MyBlock 14 | :members: 15 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/AutoFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/AutoFunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/AutoGVL.TcGVL: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 23 | 24 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/AutoStruct.TcDUT: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/FB_MyBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 21 | 22 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/PlainFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/PlainFunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/roots/test-plc-autodoc/src_plc/RegularFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(".")) 5 | 6 | extensions = ["plcdoc"] 7 | 8 | # The suffix of source filenames. 9 | source_suffix = ".rst" 10 | 11 | nitpicky = True 12 | 13 | plc_project = os.path.join(os.path.abspath("."), "src_plc/MyPLC.plcproj") 14 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/index.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEMCON/plcdoc/f6cd6881cd93e853502f183de8054e54ac008148/tests/roots/test-plc-project/index.rst -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/DUTs/E_Error.TcDUT: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/DUTs/ST_MyStruct.TcDUT: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/DUTs/T_ALIAS.TcDUT: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/MyPLC.plcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code 5 | 6 | 7 | Code 8 | 9 | 10 | Code 11 | 12 | 13 | Code 14 | 15 | 16 | Code 17 | 18 | 19 | Code 20 | 21 | 22 | Code 23 | 24 | 25 | Code 26 | 27 | 28 | Code 29 | 30 | 31 | Code 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/FB_MyBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 21 | 22 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/FB_SecondBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/F_SyntaxError.TcPOU: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/MAIN.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 15 | 16 | request THEN 17 | ack := request; 18 | END_IF 19 | 20 | result := RegularFunction(input := 3.14, other_arg := 7); 21 | ]]> 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/PlainFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/PlainFunctionBlock.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/roots/test-plc-project/src_plc/POUs/RegularFunction.TcPOU: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/roots/test-plc-ref/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(".")) 5 | 6 | extensions = ["plcdoc"] 7 | 8 | # The suffix of source filenames. 9 | source_suffix = ".rst" 10 | 11 | nitpicky = True 12 | -------------------------------------------------------------------------------- /tests/roots/test-plc-ref/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. plc:function:: MyFunction 3 | 4 | This is a reference to :plc:func:`MyFunction`. 5 | 6 | This is an invalid reference: :plc:func:`IDontExist`. 7 | 8 | .. plc:functionblock:: MyBlock 9 | 10 | This is a reference to :plc:funcblock:`MyBlock`. 11 | 12 | This is an invalid reference: :plc:func:`DoesNotExistEither`. 13 | 14 | 15 | Now test using custom types in argument or return types, in either notation: 16 | 17 | .. plc:functionblock:: BlockReturn 18 | 19 | .. plc:functionblock:: BlockArg 20 | 21 | .. plc:function:: FunctionWithMyBlockSignature(x: BlockArg) : BlockReturn 22 | 23 | 24 | .. plc:function:: FunctionWithMyBlockList(x) 25 | 26 | :param x: 27 | :type x: BlockArg 28 | :rtype: BlockReturn 29 | -------------------------------------------------------------------------------- /tests/test_domain_plc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the PLC directives directly, for manually describing objects. 3 | """ 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.sphinx("dummy", testroot="domain-plc") 9 | def test_domain_plc_objects(app, status, warning): 10 | app.builder.build_all() 11 | 12 | objects = app.env.domains["plc"].data["objects"] 13 | 14 | assert objects["BoringFunction"][2] == "function" 15 | assert objects["FB_TypedByHand"][2] == "functionblock" 16 | assert objects["FunctionBlockWithMethod"][2] == "functionblock" 17 | assert objects["E_Options"][2] == "enum" 18 | assert objects["Orientation"][2] == "enum" 19 | assert objects["ST_MyStruct"][2] == "struct" 20 | assert objects["ST_MyStruct2"][2] == "struct" 21 | assert objects["GVL_MyList"][2] == "gvl" 22 | -------------------------------------------------------------------------------- /tests/test_interpreter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the PLC interpreter on some TwinCAT source files. 3 | """ 4 | 5 | import pytest 6 | import os 7 | 8 | from plcdoc.interpreter import PlcInterpreter, PlcDeclaration 9 | 10 | 11 | CODE_DIR = os.path.join(os.path.dirname(__file__), "plc_code") 12 | 13 | 14 | @pytest.fixture() 15 | def interpreter(): 16 | return PlcInterpreter() 17 | 18 | 19 | class TestPlcInterpreter: 20 | FILES = [ 21 | "POUs/PlainFunction.TcPOU", 22 | "POUs/RegularFunction.TcPOU", 23 | "POUs/PlainFunctionBlock.TcPOU", 24 | "POUs/FB_MyBlock.TcPOU", 25 | "POUs/FB_SecondBlock.TcPOU", 26 | "POUs/MAIN.TcPOU", 27 | "DUTs/E_Options.TcDUT", 28 | "GVLs/GVL_Main.TcGVL", 29 | ] 30 | 31 | def test_files(self, interpreter): 32 | """Test content from files directly.""" 33 | files = [ 34 | os.path.join(CODE_DIR, "TwinCAT PLC", "MyPLC", file) for file in self.FILES 35 | ] 36 | interpreter.parse_source_files(files) 37 | 38 | objects = [ 39 | "PlainFunction", 40 | "RegularFunction", 41 | "PlainFunctionBlock", 42 | "FB_MyBlock", 43 | "FB_MyBlock.MyMethod", 44 | "FB_SecondBlock", 45 | "MAIN", 46 | "E_Options", 47 | "GVL_Main", 48 | ] 49 | 50 | for name in objects: 51 | try: 52 | interpreter.get_object(name) 53 | except KeyError: 54 | pytest.fail(f"Failed to get object `{name}` as expected") 55 | 56 | def test_project(self, interpreter): 57 | """Test loading contents through a PLC project file.""" 58 | file = os.path.join(CODE_DIR, "TwinCAT PLC", "MyPLC", "MyPLC.plcproj") 59 | result = interpreter.parse_plc_project(file) 60 | assert result 61 | 62 | external_projects = [ 63 | ( 64 | "extern/lcls-twincat-general/LCLSGeneral/LCLSGeneral/LCLSGeneral.plcproj", 65 | { 66 | "functionblock": 33, 67 | "struct": 12, 68 | "function": 5 + 20, # Functions + Methods 69 | "property": 1, 70 | "gvl": 4, 71 | }, 72 | ), 73 | ( 74 | "extern/lcls-twincat-motion/lcls-twincat-motion/Library/Library.plcproj", 75 | { 76 | "functionblock": 32, 77 | "struct": 5, 78 | "function": 10 + 3, 79 | "gvl": 2, 80 | }, 81 | ), 82 | ( 83 | "extern/TcUnit/TcUnit/TcUnit/TcUnit.plcproj", 84 | { 85 | "enum": 2, 86 | "struct": 8, 87 | "union": 1, 88 | "functionblock": 14 + 3, # Blocks + Interfaces 89 | "function": 36 + 138, 90 | "gvl": 3, 91 | }, 92 | ), 93 | ] 94 | 95 | @pytest.mark.skipif( 96 | not os.path.exists( 97 | os.path.join(CODE_DIR, "extern/lcls-twincat-general/LCLSGeneral") 98 | ), 99 | reason="External projects not present", 100 | ) 101 | @pytest.mark.parametrize("project,expected", external_projects) 102 | def test_large_external_projects(self, caplog, project, expected): 103 | """Test grammar on a big existing project. 104 | 105 | The goal is not so much to check the results in detail but just to make sure there are no 106 | errors, and we likely covered all possible syntax. 107 | 108 | Do not use the `interpreter` fixture as we want the object fresh each time. 109 | """ 110 | 111 | interpreter = PlcInterpreter() 112 | file = os.path.join(CODE_DIR, project) 113 | file = os.path.realpath(file) 114 | result = interpreter.parse_plc_project(file) 115 | 116 | errors = [ 117 | record.message 118 | for record in caplog.records 119 | if "Error parsing" in record.message 120 | ] 121 | assert len(errors) == 0 # Make sure no parsing errors were logged 122 | 123 | assert result 124 | for key, number in expected.items(): 125 | assert len(interpreter._models[key]) == number 126 | -------------------------------------------------------------------------------- /tests/test_plc_autodoc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the PLC auto-doc directives. 3 | 4 | The structure of these tests is largely copied from the tests of sphinx.ext.autodoc. 5 | """ 6 | 7 | import pytest 8 | from unittest.mock import Mock 9 | 10 | from sphinx import addnodes 11 | from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options 12 | from sphinx.util.docutils import LoggingReporter 13 | 14 | 15 | def do_autodoc(app, objtype, name, options=None): 16 | """Run specific autodoc function and get output. 17 | 18 | This does rely on `testroot` but no documentation is generated. 19 | """ 20 | if options is None: 21 | options = {} 22 | app.env.temp_data.setdefault("docname", "index") # set dummy docname 23 | doccls = app.registry.documenters[objtype] 24 | docoptions = process_documenter_options(doccls, app.config, options) 25 | state = Mock() 26 | state.document.settings.tab_width = 8 27 | bridge = DocumenterBridge(app.env, LoggingReporter(""), docoptions, 1, state) 28 | documenter = doccls(bridge, name) 29 | documenter.generate() 30 | 31 | return bridge.result 32 | 33 | 34 | @pytest.mark.sphinx("dummy", testroot="plc-autodoc") 35 | def test_autodoc_build(app, status, warning): 36 | """Test building a document with the PLC autodoc features.""" 37 | app.builder.build_all() 38 | 39 | content = app.env.get_doctree("index") 40 | 41 | assert isinstance(content[2], addnodes.desc) 42 | assert content[2][0].astext() == "FUNCTION RegularFunction(input, other_arg)" 43 | assert ( 44 | "Long description of the function, spanning multiple\nrows even" 45 | in content[2][1].astext() 46 | ) 47 | 48 | assert isinstance(content[4], addnodes.desc) 49 | assert content[4][0].astext() == "FUNCTION PlainFunction()" 50 | 51 | assert isinstance(content[7], addnodes.desc) 52 | assert ( 53 | content[7][0].astext() 54 | == "FUNCTION_BLOCK PlainFunctionBlock(someInput, someOutput)" 55 | ) 56 | assert "someOutput (BOOL)" in content[7][1].astext() 57 | 58 | assert isinstance(content[9], addnodes.desc) 59 | assert ( 60 | content[9][0].astext() 61 | == "FUNCTION_BLOCK FB_MyBlock(someInput, otherInput, secondClause, myOutput)" 62 | ) 63 | assert "AnotherMethod" in content[9][1].astext() 64 | assert "MyMethod" in content[9][1].astext() 65 | 66 | 67 | @pytest.mark.sphinx("html", testroot="plc-autodoc") 68 | def test_autodoc_function(app, status, warning): 69 | """Test building a document with the PLC autodoc features.""" 70 | 71 | actual = do_autodoc(app, "plc:function", "AutoFunction") 72 | 73 | assert ".. plc:function:: AutoFunction(input, other_arg)" in actual 74 | 75 | expected_end = [ 76 | " Short description of the function.", 77 | "", 78 | " Long description of the function, spanning multiple", 79 | " rows even.", 80 | "", 81 | " :var_input LREAL input: This is an in-code description of some variable", 82 | " :var_input UDINT other_arg:", 83 | "", 84 | ] 85 | 86 | assert expected_end == actual[-8:] 87 | 88 | 89 | @pytest.mark.sphinx("html", testroot="plc-autodoc") 90 | def test_autodoc_functionblock(app, status, warning): 91 | """Test building a document with the PLC autodoc features.""" 92 | 93 | options = {"members": None} 94 | actual = do_autodoc(app, "plc:functionblock", "AutoFunctionBlock", options) 95 | 96 | assert ( 97 | ".. plc:functionblock:: AutoFunctionBlock(someInput, someOutput)" == actual[1] 98 | ) 99 | assert " .. plc:method:: AutoMethod(methodInput)" == actual[10] 100 | assert " .. plc:property:: AutoProperty" == actual[18] 101 | 102 | assert [ 103 | " Some short description.", 104 | "", 105 | " :var_input BOOL someInput: Important description", 106 | " :var_output BOOL someOutput:", 107 | "", 108 | ] == actual[4:9] 109 | 110 | assert [ 111 | " Method description!", 112 | "", 113 | " :var_input UDINT methodInput:", 114 | "", 115 | ] == actual[13:17] 116 | 117 | assert [ 118 | " Reference to a variable that might be read-only.", 119 | "", 120 | ] == actual[21:] 121 | 122 | 123 | @pytest.mark.sphinx("html", testroot="plc-autodoc") 124 | def test_autodoc_struct(app, status, warning): 125 | """Test building a document with the PLC autodoc features.""" 126 | 127 | actual = do_autodoc(app, "plc:struct", "AutoStruct") 128 | 129 | assert ".. plc:struct:: AutoStruct" == actual[1] 130 | assert " .. plc:member:: someDouble : LREAL" == actual[7] 131 | assert " .. plc:member:: someBoolean : BOOL" == actual[13] 132 | 133 | assert [ 134 | " A definition of a struct.", 135 | "", 136 | ] == actual[4:6] 137 | 138 | assert [ 139 | " Use to store a number", 140 | "", 141 | ] == actual[10:12] 142 | 143 | assert [ 144 | " Use as a flag", 145 | "", 146 | ] == actual[16:] 147 | 148 | 149 | @pytest.mark.sphinx("html", testroot="plc-autodoc") 150 | def test_autodoc_gvl(app, status, warning): 151 | """Test building a document with the PLC autodoc features.""" 152 | 153 | actual = do_autodoc(app, "plc:gvl", "AutoGVL") 154 | 155 | assert ".. plc:gvl:: AutoGVL" == actual[1] 156 | assert " :var BOOL flag: Flag for the system" == actual[4] 157 | assert " :var ULINT counter:" == actual[5] 158 | assert " :var LREAL cycleTime: Time between PLC cycles" == actual[6] 159 | assert " :var BOOL otherSection:" == actual[7] 160 | assert " :var INT MY_CONST:" == actual[8] 161 | assert " :var UDINT runtime_sec:" == actual[9] 162 | -------------------------------------------------------------------------------- /tests/test_plc_project.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test PLC documentation when loading in a *.plcproj file. 3 | """ 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.sphinx("dummy", testroot="plc-project") 9 | def test_project_interpret(app, status, warning): 10 | """Test building a document loading a project.""" 11 | 12 | assert hasattr(app, "_interpreter") 13 | 14 | interpreter = getattr(app, "_interpreter") 15 | 16 | expected = { 17 | "functionblock": ["FB_MyBlock", "FB_SecondBlock", "PlainFunctionBlock"], 18 | "function": ["PlainFunction", "RegularFunction"], 19 | "program": ["MAIN"], 20 | "enum": ["E_Error"], 21 | "struct": ["ST_MyStruct"], 22 | "alias": ["T_ALIAS"], 23 | } 24 | 25 | for objtype, objects in expected.items(): 26 | for obj in objects: 27 | try: 28 | declaration = interpreter.get_object(obj, objtype) 29 | assert declaration is not None 30 | except KeyError: 31 | pytest.fail(f"Could not find expected object {obj}") 32 | 33 | 34 | @pytest.mark.sphinx("dummy", testroot="plc-project") 35 | def test_project_build(app, status, warning, caplog): 36 | """Test building a document loading a project.""" 37 | app.builder.build_all() 38 | # Project contains a function with an outright syntax error, but the project 39 | # completes nonetheless. 40 | -------------------------------------------------------------------------------- /tests/test_plc_ref.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the PLC directives directly, for manually describing objects. 3 | """ 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.sphinx("dummy", testroot="plc-ref") 9 | def test_plc_ref_build(app, status, warning): 10 | app.builder.build_all() 11 | 12 | warning_str = warning.getvalue() 13 | 14 | assert "IDontExist" in warning_str 15 | assert "MyFunction" not in warning_str 16 | 17 | assert "DoesNotExistEither" in warning_str 18 | assert "MyBlock" not in warning_str 19 | 20 | assert "BlockReturn" not in warning_str 21 | assert "BlockArg" not in warning_str 22 | -------------------------------------------------------------------------------- /tests/test_st_grammar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run the TextX grammar over some PLC code to see if it works. 3 | """ 4 | 5 | import pytest 6 | 7 | import os 8 | from textx import metamodel_from_file, TextXSyntaxError 9 | import re 10 | 11 | 12 | tests_dir = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | pattern_ws = re.compile(r"\s+") # Match any whitespace 15 | 16 | 17 | @pytest.fixture() 18 | def meta_model(): 19 | txpath = os.path.realpath(tests_dir + "/../src/plcdoc/st_declaration.tx") 20 | return metamodel_from_file(txpath) 21 | 22 | 23 | grammar_files = [ 24 | "FB_MyBlock.txt", 25 | "FB_MyBlockExtended.txt", 26 | "RegularFunction.txt", 27 | "MyStructure.txt", 28 | "MyStructureExtended.txt", 29 | "E_Options.txt", 30 | "E_Filter.txt", 31 | "Properties.txt", 32 | "Unions.txt", 33 | "T_ALIAS.txt", 34 | "GlobalVariableList.txt", 35 | "Main.txt", 36 | ] 37 | 38 | 39 | @pytest.mark.parametrize("file", grammar_files) 40 | def test_grammar_on_files(meta_model, file): 41 | """Test if a range of files can all be parsed without errors.""" 42 | filepath = os.path.realpath(tests_dir + "/plc_code/" + file) 43 | try: 44 | model = meta_model.model_from_file(filepath) 45 | except TextXSyntaxError as err: # noqa 46 | pytest.fail(f"Error when analyzing the file `{file}`: {err}") 47 | else: 48 | assert model is not None 49 | assert ( 50 | model.functions or model.types or model.properties or model.variable_lists 51 | ) 52 | 53 | 54 | def remove_whitespace(text): 55 | if isinstance(text, str): 56 | return re.sub(pattern_ws, "", text) 57 | 58 | return text 59 | 60 | 61 | def assert_variable(var, expected): 62 | """Verify the result of a declared variable. 63 | 64 | All whitespace is removed first. 65 | """ 66 | assert var.name == expected[0] 67 | assert var.type.name.strip() == expected[1] 68 | assert remove_whitespace(var.value) == expected[2] 69 | assert remove_whitespace(var.arglist) == expected[3] 70 | assert remove_whitespace(var.type.array) == expected[4] 71 | assert var.type.pointer == expected[5] 72 | 73 | 74 | def test_grammar_variables(meta_model): 75 | """Test variables in declarations specifically. 76 | 77 | PLC can create different variables in different ways, test all of them here. 78 | """ 79 | filename = "FB_Variables.txt" 80 | filepath = os.path.realpath(tests_dir + "/plc_code/" + filename) 81 | try: 82 | model = meta_model.model_from_file(filepath) 83 | except TextXSyntaxError as err: 84 | pytest.fail(f"Error when analyzing the file `{filename}`: {err}") 85 | else: 86 | assert model.functions 87 | fb = model.functions[0] 88 | assert fb.lists 89 | variables = fb.lists[0].variables 90 | 91 | expected_list = [ 92 | # (Name, BaseType, Value, ArgList, Array, Pointer) 93 | ("myfloat_no_ws", "REAL", None, None, None, None), 94 | ("myfloat", "REAL", None, None, None, None), 95 | ("mydoubleinit1", "LREAL", "1.0", None, None, None), 96 | ("mydoubleinit2", "LREAL", "1.0", None, None, None), 97 | ("myinteger", "SINT", "420", None, None, None), 98 | ("mystring", "STRING", "'test'", None, None, None), 99 | ("int_arth_plus", "INT", "1+1", None, None, None), 100 | ("int_arth_minus", "INT", "10-1", None, None, None), 101 | ("int_arth_parent", "INT", "1+(LIM+1)*5", None, None, None), 102 | ("my_object", "MyObject", None, "()", None, None), 103 | ("my_object1", "MyObject", None, "(7)", None, None), 104 | ("my_object2", "MyObject", None, "('hi',23,FALSE)", None, None), 105 | ( 106 | "my_object3", 107 | "MyObject", 108 | None, 109 | "(text:='hi',number:=23,flag:=FALSE)", 110 | None, 111 | None, 112 | ), 113 | ( 114 | "my_object4", 115 | "MyObject", 116 | None, 117 | "(text:='hi',number:=23,flag:=FALSE)", 118 | None, 119 | None, 120 | ), 121 | ("mystring_size1", "STRING(15)", None, None, None, None), 122 | ("mystring_size2", "STRING[17]", None, None, None, None), 123 | ("mystring_size3", "STRING(Module.SIZE)", None, None, None, None), 124 | ("mystring_size4", "STRING[SIZE]", None, None, None, None), 125 | ("mystring_size5", "STRING(35)", "'Unknown'", None, None, None), 126 | ("mystring_size6", "STRING[Module.SIZE]", "'Unknown'", None, None, None), 127 | ("mystring_escape", "STRING", "':;\"'", None, None, None), 128 | ("mystring_concat", "STRING", "CONCAT('abc','xyz')", None, None, None), 129 | ("myint", "INT", "SomeConstant", None, None, None), 130 | ("myint2", "INT", "E_Error.NoError", None, None, None), 131 | ("mylist", "BOOL", None, None, "0..4", None), 132 | ("mylist_ws", "BOOL", None, None, "0..4", None), 133 | ("mylist_var_idx", "BOOL", None, None, "Idx.start..Idx.end", None), 134 | ("mylist_var_sum", "BOOL", None, None, "0..MAX-1", None), 135 | ("mylist_multi", "BOOL", None, None, "1..10,1..10", None), 136 | ("mylist_multi2", "BOOL", None, None, "1..10,1..10", None), 137 | ("mylist_dyn", "BOOL", None, None, "*", None), 138 | ("mylist_dyn_multi", "BOOL", None, None, "*,*,*", None), 139 | ("mystruct", "MyStruct", None, "()", None, None), 140 | ("mystruct2", "MyStruct", "(number:=1.0,text:='hi')", None, None, None), 141 | ("specialint1", "UDINT", "2#1001_0110", None, None, None), 142 | ("specialint2", "UDINT", "8#67", None, None, None), 143 | ("specialint3", "UDINT", "16#FF_FF_FF", None, None, None), 144 | ("specialint4", "UDINT", "UDINT#16#1", None, None, None), 145 | ("specialint5", "UDINT", "1_000_000", None, None, None), 146 | ("specialint6", "USINT", "(1..Module.MAX)", None, None, None), 147 | ("mypointer1", "UDINT", None, None, None, "POINTER"), 148 | ("mypointer2", "UDINT", None, None, None, "REFERENCE"), 149 | ("mypointer3", "FB_Motor", "_motor", None, None, "REFERENCE"), 150 | ("extra_semicolons", "INT", "7", None, None, None), 151 | ("timeout1", "TIME", "T#2S", None, None, None), 152 | ("timeout2", "TIME", "T#12m13s14ms", None, None, None), 153 | ("inline1", "INT", None, None, None, None), 154 | ] 155 | 156 | assert len(variables) == 47 157 | 158 | for i, expected in enumerate(expected_list): 159 | assert_variable(variables[i], expected) 160 | 161 | 162 | def test_grammar_comments(meta_model): 163 | """Test grammar on a file with a lot of comments. 164 | 165 | Some comments are important while some can be discarded. 166 | """ 167 | filename = "FB_Comments.txt" 168 | filepath = os.path.realpath(tests_dir + "/plc_code/" + filename) 169 | try: 170 | model = meta_model.model_from_file(filepath) 171 | except: 172 | pytest.fail(f"Error when analyzing the file `{filename}`") 173 | else: 174 | assert model is not None 175 | assert model.functions and not model.types 176 | fb = model.functions[0] 177 | 178 | # Make sure the real code came through properly: 179 | assert fb.name == "FB_MyBlock" and fb.function_type == "FUNCTION_BLOCK" 180 | assert len(fb.lists) == 4 and [l.name for l in fb.lists] == [ 181 | "VAR_INPUT", 182 | "VAR_OUTPUT", 183 | "VAR", 184 | "VAR", 185 | ] 186 | assert [l.constant for l in fb.lists] == [False, False, True, False] 187 | 188 | # All the fields containing a doc-comment: 189 | list_important = [ 190 | fb.comments[-1].text, 191 | fb.lists[0].variables[0].comment.text, 192 | fb.lists[0].variables[1].comment.text, 193 | fb.lists[1].variables[0].comment.text, 194 | fb.lists[2].variables[0].comment.text, 195 | fb.lists[3].variables[0].comment.text, 196 | ] 197 | 198 | for comment in list_important: 199 | assert "Important" in comment and "Ignored" not in comment 200 | # We don't verify the content of ignored comments, they might disappear in the 201 | # future 202 | 203 | # Check attribute came over: 204 | attributes = [c for c in fb.comments if type(c).__name__ == "Attribute"] 205 | assert len(attributes) == 1 and attributes[0].name == "naming" 206 | 207 | 208 | # Also see `test_large_external_projects` for more massive tests 209 | --------------------------------------------------------------------------------