├── .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 | [](https://plc-doc.readthedocs.io/latest/?badge=latest)
4 | [](https://github.com/DEMCON/plcdoc/actions)
5 | [](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 |
--------------------------------------------------------------------------------