abap-parser 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {abap_parser-0.3.0 → abap_parser-0.4.0}/Cargo.lock +9 -9
- {abap_parser-0.3.0 → abap_parser-0.4.0}/Cargo.toml +1 -1
- {abap_parser-0.3.0 → abap_parser-0.4.0}/PKG-INFO +4 -1
- {abap_parser-0.3.0 → abap_parser-0.4.0}/README.md +3 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/README.md +3 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/src/lib.rs +39 -5
- {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/tests/test_symbols.py +28 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/db.rs +5 -0
- abap_parser-0.4.0/crates/abap-analysis/src/hr.rs +333 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/lib.rs +95 -32
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/logic.rs +643 -62
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/stores.rs +54 -12
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/symbols.rs +42 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/text.rs +38 -3
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/workspace.rs +48 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/adt_mock.rs +75 -1
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/call_graph_corpus.rs +3 -2
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/symbols_diff.rs +3 -2
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-lexer/src/lib.rs +12 -5
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-model/src/lib.rs +110 -2
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/adt.rs +630 -81
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/badi.rs +6 -1
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/cache.rs +16 -3
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/fs.rs +6 -6
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/lib.rs +62 -1
- abap_parser-0.4.0/crates/abap-source/src/mock/tables.rs +535 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/mock.rs +362 -2
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/options.rs +13 -1
- abap_parser-0.4.0/crates/abap-sql/src/cluster.rs +159 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/lib.rs +5 -1
- abap_parser-0.4.0/crates/abap-syntax/src/check.rs +479 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/classify.rs +26 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/lib.rs +73 -4
- abap_parser-0.4.0/crates/abap-syntax/src/release.rs +1418 -0
- abap_parser-0.4.0/crates/abap-syntax/src/system.rs +276 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/abaplint_statements.rs +15 -2
- abap_parser-0.4.0/crates/abap-syntax/tests/release_diff.rs +295 -0
- abap_parser-0.4.0/crates/abap-syntax/tests/syntax_errors.rs +162 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/python/abap_parser/__init__.py +28 -6
- {abap_parser-0.3.0 → abap_parser-0.4.0}/python/abap_parser/types.py +72 -10
- {abap_parser-0.3.0 → abap_parser-0.4.0}/LICENSE +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/LICENSE +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/args.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/calls.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/context.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/ddic.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/enhance.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/logic_corpus.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-lexer/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-model/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/cds.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/hana.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/tests.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/tests/cds_corpus.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/Cargo.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/decl.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/expr.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/kind.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/operands.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/structure.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/pyproject.toml +0 -0
- {abap_parser-0.3.0 → abap_parser-0.4.0}/python/abap_parser/py.typed +0 -0
|
@@ -4,7 +4,7 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "abap-analysis"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"abap-model",
|
|
10
10
|
"abap-source",
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
|
|
17
17
|
[[package]]
|
|
18
18
|
name = "abap-cli"
|
|
19
|
-
version = "0.
|
|
19
|
+
version = "0.4.0"
|
|
20
20
|
dependencies = [
|
|
21
21
|
"abap-analysis",
|
|
22
22
|
"abap-lexer",
|
|
@@ -28,14 +28,14 @@ dependencies = [
|
|
|
28
28
|
|
|
29
29
|
[[package]]
|
|
30
30
|
name = "abap-lexer"
|
|
31
|
-
version = "0.
|
|
31
|
+
version = "0.4.0"
|
|
32
32
|
dependencies = [
|
|
33
33
|
"serde",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "abap-model"
|
|
38
|
-
version = "0.
|
|
38
|
+
version = "0.4.0"
|
|
39
39
|
dependencies = [
|
|
40
40
|
"schemars",
|
|
41
41
|
"serde",
|
|
@@ -44,7 +44,7 @@ dependencies = [
|
|
|
44
44
|
|
|
45
45
|
[[package]]
|
|
46
46
|
name = "abap-node"
|
|
47
|
-
version = "0.
|
|
47
|
+
version = "0.4.0"
|
|
48
48
|
dependencies = [
|
|
49
49
|
"abap-analysis",
|
|
50
50
|
"napi",
|
|
@@ -55,7 +55,7 @@ dependencies = [
|
|
|
55
55
|
|
|
56
56
|
[[package]]
|
|
57
57
|
name = "abap-python"
|
|
58
|
-
version = "0.
|
|
58
|
+
version = "0.4.0"
|
|
59
59
|
dependencies = [
|
|
60
60
|
"abap-analysis",
|
|
61
61
|
"pyo3",
|
|
@@ -63,7 +63,7 @@ dependencies = [
|
|
|
63
63
|
|
|
64
64
|
[[package]]
|
|
65
65
|
name = "abap-source"
|
|
66
|
-
version = "0.
|
|
66
|
+
version = "0.4.0"
|
|
67
67
|
dependencies = [
|
|
68
68
|
"base64 0.22.1",
|
|
69
69
|
"serde",
|
|
@@ -73,7 +73,7 @@ dependencies = [
|
|
|
73
73
|
|
|
74
74
|
[[package]]
|
|
75
75
|
name = "abap-sql"
|
|
76
|
-
version = "0.
|
|
76
|
+
version = "0.4.0"
|
|
77
77
|
dependencies = [
|
|
78
78
|
"abap-model",
|
|
79
79
|
"abap-syntax",
|
|
@@ -83,7 +83,7 @@ dependencies = [
|
|
|
83
83
|
|
|
84
84
|
[[package]]
|
|
85
85
|
name = "abap-syntax"
|
|
86
|
-
version = "0.
|
|
86
|
+
version = "0.4.0"
|
|
87
87
|
dependencies = [
|
|
88
88
|
"abap-lexer",
|
|
89
89
|
"serde",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/abap-lexer", "crates/abap-syntax", "crates/abap-model", "crates/abap-sql", "crates/abap-source", "crates/abap-analysis", "bindings/python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.4.0"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
repository = "https://github.com/albertlast/abapPaser"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abap-parser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Classifier: License :: OSI Approved :: MIT License
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -50,6 +50,9 @@ prompt = abap_parser.text("logic", source)
|
|
|
50
50
|
outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
|
|
51
51
|
# a CDS view: sources, joins, associations, the fields of each element
|
|
52
52
|
view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
|
|
53
|
+
# sources of the host, e.g. from its own cache: asked after the folders,
|
|
54
|
+
# before the SAP system; returns the source or None
|
|
55
|
+
own = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], {"fetch": lambda ref: cache.get(ref)}, depth=2)
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
Every function has an `..._async` variant for asyncio; the native code releases the GIL. The results are dictionaries typed by the TypedDicts in `abap_parser.types`. `text` and `object_text` give a compact form instead: definitions written like ABAP, one line per call or database access, per procedure its effects with statements and conditions. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
|
|
@@ -33,6 +33,9 @@ prompt = abap_parser.text("logic", source)
|
|
|
33
33
|
outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
|
|
34
34
|
# a CDS view: sources, joins, associations, the fields of each element
|
|
35
35
|
view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
|
|
36
|
+
# sources of the host, e.g. from its own cache: asked after the folders,
|
|
37
|
+
# before the SAP system; returns the source or None
|
|
38
|
+
own = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], {"fetch": lambda ref: cache.get(ref)}, depth=2)
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
Every function has an `..._async` variant for asyncio; the native code releases the GIL. The results are dictionaries typed by the TypedDicts in `abap_parser.types`. `text` and `object_text` give a compact form instead: definitions written like ABAP, one line per call or database access, per procedure its effects with statements and conditions. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
|
|
@@ -33,6 +33,9 @@ prompt = abap_parser.text("logic", source)
|
|
|
33
33
|
outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
|
|
34
34
|
# a CDS view: sources, joins, associations, the fields of each element
|
|
35
35
|
view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
|
|
36
|
+
# sources of the host, e.g. from its own cache: asked after the folders,
|
|
37
|
+
# before the SAP system; returns the source or None
|
|
38
|
+
own = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], {"fetch": lambda ref: cache.get(ref)}, depth=2)
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
Every function has an `..._async` variant for asyncio; the native code releases the GIL. The results are dictionaries typed by the TypedDicts in `abap_parser.types`. `text` and `object_text` give a compact form instead: definitions written like ABAP, one line per call or database access, per procedure its effects with statements and conditions. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
//! Native part of the Python package. Results cross the boundary as JSON
|
|
2
2
|
//! strings, abap_parser/__init__.py parses them, so the shape is exactly the
|
|
3
3
|
//! Rust model.
|
|
4
|
+
//!
|
|
5
|
+
//! Functions loading sources take a `fetch` callable of the host; the
|
|
6
|
+
//! native code runs without the GIL and takes it to call it.
|
|
4
7
|
|
|
5
8
|
use pyo3::prelude::*;
|
|
6
9
|
|
|
7
10
|
#[pymodule]
|
|
8
11
|
mod _native {
|
|
12
|
+
use abap_analysis::abap_source::{FnProvider, SourceError};
|
|
13
|
+
use abap_analysis::json::Host;
|
|
9
14
|
use pyo3::prelude::*;
|
|
10
15
|
|
|
16
|
+
/// `fetch(reference) -> str | None` as a source provider.
|
|
17
|
+
fn host(fetch: Option<Py<PyAny>>) -> Host {
|
|
18
|
+
let fetch = fetch?;
|
|
19
|
+
Some(Box::new(FnProvider::new("fetch", move |r| {
|
|
20
|
+
Python::attach(|py| {
|
|
21
|
+
fetch
|
|
22
|
+
.call1(py, (r.to_string(),))
|
|
23
|
+
.and_then(|v| v.bind(py).extract::<Option<String>>())
|
|
24
|
+
})
|
|
25
|
+
.map_err(|e| SourceError::Other(format!("fetch {r}: {e}")))
|
|
26
|
+
})))
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
#[pyfunction]
|
|
12
30
|
fn symbols_json(py: Python<'_>, source: &str) -> String {
|
|
13
31
|
py.detach(|| abap_analysis::json::symbols(source))
|
|
@@ -24,19 +42,29 @@ mod _native {
|
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
#[pyfunction]
|
|
45
|
+
#[pyo3(signature = (roots, options, depth, fetch=None))]
|
|
27
46
|
fn call_graph_json(
|
|
28
47
|
py: Python<'_>,
|
|
29
48
|
roots: Vec<String>,
|
|
30
49
|
options: &str,
|
|
31
50
|
depth: u32,
|
|
51
|
+
fetch: Option<Py<PyAny>>,
|
|
32
52
|
) -> PyResult<String> {
|
|
33
|
-
|
|
53
|
+
let host = host(fetch);
|
|
54
|
+
py.detach(|| abap_analysis::json::call_graph(&roots, options, depth, host))
|
|
34
55
|
.map_err(pyo3::exceptions::PyRuntimeError::new_err)
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
#[pyfunction]
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
#[pyo3(signature = (reference, options, fetch=None))]
|
|
60
|
+
fn fetch_source(
|
|
61
|
+
py: Python<'_>,
|
|
62
|
+
reference: &str,
|
|
63
|
+
options: &str,
|
|
64
|
+
fetch: Option<Py<PyAny>>,
|
|
65
|
+
) -> PyResult<Option<String>> {
|
|
66
|
+
let host = host(fetch);
|
|
67
|
+
py.detach(|| abap_analysis::json::fetch(reference, options, host))
|
|
40
68
|
.map_err(pyo3::exceptions::PyRuntimeError::new_err)
|
|
41
69
|
}
|
|
42
70
|
|
|
@@ -46,13 +74,16 @@ mod _native {
|
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
#[pyfunction]
|
|
77
|
+
#[pyo3(signature = (reference, options, depth, fetch=None))]
|
|
49
78
|
fn logic_object_json(
|
|
50
79
|
py: Python<'_>,
|
|
51
80
|
reference: &str,
|
|
52
81
|
options: &str,
|
|
53
82
|
depth: u32,
|
|
83
|
+
fetch: Option<Py<PyAny>>,
|
|
54
84
|
) -> PyResult<String> {
|
|
55
|
-
|
|
85
|
+
let host = host(fetch);
|
|
86
|
+
py.detach(|| abap_analysis::json::logic_object(reference, options, depth, host))
|
|
56
87
|
.map_err(pyo3::exceptions::PyRuntimeError::new_err)
|
|
57
88
|
}
|
|
58
89
|
|
|
@@ -73,14 +104,17 @@ mod _native {
|
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
#[pyfunction]
|
|
107
|
+
#[pyo3(signature = (analysis, objects, options, depth, fetch=None))]
|
|
76
108
|
fn object_text(
|
|
77
109
|
py: Python<'_>,
|
|
78
110
|
analysis: &str,
|
|
79
111
|
objects: Vec<String>,
|
|
80
112
|
options: &str,
|
|
81
113
|
depth: u32,
|
|
114
|
+
fetch: Option<Py<PyAny>>,
|
|
82
115
|
) -> PyResult<String> {
|
|
83
|
-
|
|
116
|
+
let host = host(fetch);
|
|
117
|
+
py.detach(|| abap_analysis::json::object_text(analysis, &objects, options, depth, host))
|
|
84
118
|
.map_err(pyo3::exceptions::PyRuntimeError::new_err)
|
|
85
119
|
}
|
|
86
120
|
|
|
@@ -118,6 +118,34 @@ class SymbolsTest(unittest.TestCase):
|
|
|
118
118
|
with self.assertRaises(RuntimeError):
|
|
119
119
|
abap_parser.object_text("graph", ["CLAS:X"], {})
|
|
120
120
|
|
|
121
|
+
def test_sources_from_a_fetch_function(self):
|
|
122
|
+
sources = {
|
|
123
|
+
"PROG:ZMAIN": "REPORT zmain.\nSTART-OF-SELECTION.\n CALL FUNCTION 'Z_SAVE'.\n PERFORM run IN PROGRAM zother.",
|
|
124
|
+
"FUNC:Z_SAVE": "FUNCTION z_save.\n DELETE FROM zlog WHERE id = 1.\nENDFUNCTION.",
|
|
125
|
+
}
|
|
126
|
+
asked = []
|
|
127
|
+
|
|
128
|
+
def fetch(reference):
|
|
129
|
+
asked.append(reference)
|
|
130
|
+
return sources.get(reference)
|
|
131
|
+
|
|
132
|
+
graph = abap_parser.call_graph(["PROG:ZMAIN"], {"fetch": fetch}, 2)
|
|
133
|
+
self.assertIn(("FUNC:Z_SAVE", "fetch:FUNC:Z_SAVE"), [(u["reference"], u["origin"]) for u in graph["units"]])
|
|
134
|
+
self.assertIn("PROG:ZOTHER", graph["missing"])
|
|
135
|
+
self.assertIn("FUNC:Z_SAVE", asked)
|
|
136
|
+
lg = abap_parser.logic_object("PROG:ZMAIN", {"fetch": fetch})
|
|
137
|
+
self.assertIn("ZLOG", [e["name"] for e in lg["procedures"][0]["effects"]])
|
|
138
|
+
self.assertEqual(abap_parser.fetch_source("PROG:ZMAIN", {"fetch": fetch}), sources["PROG:ZMAIN"])
|
|
139
|
+
self.assertIn("z_save", abap_parser.object_text("symbols", ["FUNC:Z_SAVE"], {"fetch": fetch}).lower())
|
|
140
|
+
|
|
141
|
+
def failing(reference):
|
|
142
|
+
raise ConnectionError("no connection")
|
|
143
|
+
|
|
144
|
+
graph = abap_parser.call_graph(["PROG:ZMAIN"], {"fetch": failing})
|
|
145
|
+
self.assertTrue(any("no connection" in e for e in graph["errors"]), graph["errors"])
|
|
146
|
+
with self.assertRaises(TypeError):
|
|
147
|
+
abap_parser.call_graph(["PROG:ZMAIN"], {"fetch": "nope"})
|
|
148
|
+
|
|
121
149
|
@unittest.skipUnless((CORPUS / "abapgit" / "src").exists(), "corpus submodule missing")
|
|
122
150
|
def test_real_class(self):
|
|
123
151
|
path = CORPUS / "abapgit" / "src" / "apack" / "zcl_abapgit_apack_migration.clas.abap"
|
|
@@ -35,6 +35,11 @@ pub(crate) fn db_access_with(file: &File, symbols: &m::FileSymbols) -> Vec<m::Db
|
|
|
35
35
|
resolve_dynamic(&mut a, &constants);
|
|
36
36
|
out.push(a);
|
|
37
37
|
}
|
|
38
|
+
} else if let Some(mut a) = abap_sql::parse_cluster(file, s) {
|
|
39
|
+
a.block = blocks[i].clone();
|
|
40
|
+
a.text = file.statement_text(s);
|
|
41
|
+
a.span = span(file, i, i);
|
|
42
|
+
out.push(a);
|
|
38
43
|
} else if let Some(end) = native_end(file, i) {
|
|
39
44
|
// EXEC SQL ... ENDEXEC, or the body of an AMDP method: HANA SQL
|
|
40
45
|
let (from, to) = native_range(file, i, end);
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! HR infotypes: the database tables of infotypes, `PAnnnn`, `PBnnnn` and
|
|
2
|
+
//! `HRPnnnn`, read through the logical databases PNP and PCH,
|
|
3
|
+
//! `HR_READ_INFOTYPE`, `RH_READ_INFTY` and the macros of PNP, and what those
|
|
4
|
+
//! macros read and write.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
|
|
8
|
+
use abap_model as m;
|
|
9
|
+
use abap_syntax::{File, Statement, StatementKind as K, Token, TokenKind};
|
|
10
|
+
|
|
11
|
+
use crate::stores::raw;
|
|
12
|
+
|
|
13
|
+
/// What reads an infotype: it decides the table of customer infotypes,
|
|
14
|
+
/// which may be of either kind.
|
|
15
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
16
|
+
pub(crate) enum Reader {
|
|
17
|
+
/// personnel administration: PNP, `HR_READ_INFOTYPE`
|
|
18
|
+
Personnel,
|
|
19
|
+
/// applicants: `HR_READ_INFOTYPE` with `tclas = 'B'`
|
|
20
|
+
Applicant,
|
|
21
|
+
/// personnel planning: PCH, `RH_READ_INFTY`
|
|
22
|
+
Planning,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The database table of an infotype; `None` unless `number` is four
|
|
26
|
+
/// digits. The number range decides: personnel planning (1000 to 1999, and
|
|
27
|
+
/// 5000 to 5999) is in `HRPnnnn`, recruitment (4000 to 4999) in `PBnnnn`,
|
|
28
|
+
/// personnel administration and time management (0000 to 0999, 2000 to
|
|
29
|
+
/// 2999) in `PAnnnn`, or `PBnnnn` for applicants. Others, like customer
|
|
30
|
+
/// infotypes 9000 to 9999, can be of either kind: the reader decides.
|
|
31
|
+
pub(crate) fn table(number: &str, reader: Reader) -> Option<String> {
|
|
32
|
+
if number.len() != 4 || !number.bytes().all(|b| b.is_ascii_digit()) {
|
|
33
|
+
return None;
|
|
34
|
+
}
|
|
35
|
+
let n: u16 = number.parse().ok()?;
|
|
36
|
+
let prefix = match (n, reader) {
|
|
37
|
+
(1000..=1999 | 5000..=5999, _) => "HRP",
|
|
38
|
+
(4000..=4999, _) => "PB",
|
|
39
|
+
(_, Reader::Applicant) => "PB",
|
|
40
|
+
(0..=999 | 2000..=2999, _) | (_, Reader::Personnel) => "PA",
|
|
41
|
+
(_, Reader::Planning) => "HRP",
|
|
42
|
+
};
|
|
43
|
+
Some(format!("{prefix}{number}"))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// An infotype declared with INFOTYPES.
|
|
47
|
+
pub(crate) struct Infotype {
|
|
48
|
+
/// the internal table, lower case, like `p0001`
|
|
49
|
+
pub table: String,
|
|
50
|
+
pub number: String,
|
|
51
|
+
/// `MODE N`: the logical database does not fill the table
|
|
52
|
+
pub mode_n: bool,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The infotypes a program declares.
|
|
56
|
+
pub(crate) fn infotypes(symbols: &m::FileSymbols) -> Vec<Infotype> {
|
|
57
|
+
symbols
|
|
58
|
+
.declarations
|
|
59
|
+
.iter()
|
|
60
|
+
.filter(|d| d.kind == m::DeclarationKind::Infotype)
|
|
61
|
+
.filter_map(|d| {
|
|
62
|
+
let typing = d.typing.as_ref()?;
|
|
63
|
+
let number = typing.name.as_deref()?.get(1..)?.to_string();
|
|
64
|
+
let words: Vec<String> = typing
|
|
65
|
+
.text
|
|
66
|
+
.split_whitespace()
|
|
67
|
+
.map(str::to_ascii_uppercase)
|
|
68
|
+
.collect();
|
|
69
|
+
Some(Infotype {
|
|
70
|
+
table: d.name.to_ascii_lowercase(),
|
|
71
|
+
number,
|
|
72
|
+
mode_n: words.windows(2).any(|w| w[0] == "MODE" && w[1] == "N"),
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
.collect()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn read(name: String) -> m::TableAccess {
|
|
79
|
+
m::TableAccess {
|
|
80
|
+
name,
|
|
81
|
+
access: m::AccessKind::Read,
|
|
82
|
+
operation: m::DbOperation::Select,
|
|
83
|
+
store: None,
|
|
84
|
+
via: None,
|
|
85
|
+
depends: None,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// The node of `GET node`, lower case, unless it is `GET node LATE`, which
|
|
90
|
+
/// reads nothing.
|
|
91
|
+
pub(crate) fn get_node(file: &File, s: &Statement) -> Option<String> {
|
|
92
|
+
if s.kind != K::Get {
|
|
93
|
+
return None;
|
|
94
|
+
}
|
|
95
|
+
let words: Vec<&Token> = s.tokens.iter().map(|t| &file.tokens()[*t]).collect();
|
|
96
|
+
if words.iter().any(|t| t.is("LATE")) {
|
|
97
|
+
return None;
|
|
98
|
+
}
|
|
99
|
+
words.get(1).map(|t| t.text.to_ascii_lowercase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// The infotype tables the logical database fills at `GET node`, with the
|
|
103
|
+
/// database table of each: PNP at `GET PERNR` and PNPCE at `GET PERAS`
|
|
104
|
+
/// those in `PAnnnn`, PCH at `GET OBJEC` those in `HRPnnnn`; not those
|
|
105
|
+
/// declared with `MODE N`.
|
|
106
|
+
pub(crate) fn filled<'a>(node: &str, declared: &'a [Infotype]) -> Vec<(&'a Infotype, String)> {
|
|
107
|
+
let (reader, prefix) = match node {
|
|
108
|
+
"pernr" | "peras" => (Reader::Personnel, "PA"),
|
|
109
|
+
"objec" => (Reader::Planning, "HRP"),
|
|
110
|
+
_ => return Vec::new(),
|
|
111
|
+
};
|
|
112
|
+
declared
|
|
113
|
+
.iter()
|
|
114
|
+
.filter(|i| !i.mode_n)
|
|
115
|
+
.filter_map(|i| table(&i.number, reader).map(|t| (i, t)))
|
|
116
|
+
.filter(|(_, t)| t.starts_with(prefix))
|
|
117
|
+
.collect()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// An infotype number as written: digits, a literal or a constant.
|
|
121
|
+
fn number(word: &str, constants: &HashMap<String, String>) -> String {
|
|
122
|
+
if word.bytes().all(|b| b.is_ascii_digit()) {
|
|
123
|
+
word.to_string()
|
|
124
|
+
} else {
|
|
125
|
+
raw(word, constants)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Function modules reading infotypes: name, reader, and the infotype when
|
|
130
|
+
/// the name fixes it; else it is the parameter `infty`.
|
|
131
|
+
const READERS: &[(&str, Reader, Option<&str>)] = &[
|
|
132
|
+
("HR_READ_INFOTYPE", Reader::Personnel, None),
|
|
133
|
+
("RH_READ_INFTY", Reader::Planning, None),
|
|
134
|
+
("RH_READ_INFTY_1001", Reader::Planning, Some("1001")),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/// The infotype tables a statement reads from the database: `GET PERNR`
|
|
138
|
+
/// and `GET OBJEC` those of the declared infotypes, `HR_READ_INFOTYPE`,
|
|
139
|
+
/// `RH_READ_INFTY` and `RP-READ-INFOTYPE` the one they name.
|
|
140
|
+
pub(crate) fn infotype_reads(
|
|
141
|
+
file: &File,
|
|
142
|
+
s: &Statement,
|
|
143
|
+
constants: &HashMap<String, String>,
|
|
144
|
+
declared: &[Infotype],
|
|
145
|
+
) -> Vec<m::TableAccess> {
|
|
146
|
+
let words: Vec<&str> = s
|
|
147
|
+
.tokens
|
|
148
|
+
.iter()
|
|
149
|
+
.map(|t| file.tokens()[*t].text.as_str())
|
|
150
|
+
.collect();
|
|
151
|
+
match s.kind {
|
|
152
|
+
K::Get => get_node(file, s)
|
|
153
|
+
.map(|n| filled(&n, declared))
|
|
154
|
+
.unwrap_or_default()
|
|
155
|
+
.into_iter()
|
|
156
|
+
.map(|(_, t)| read(t))
|
|
157
|
+
.collect(),
|
|
158
|
+
// CALL FUNCTION 'HR_READ_INFOTYPE' EXPORTING ... infty = '0001'
|
|
159
|
+
K::CallFunction => {
|
|
160
|
+
let Some(&(_, reader, fixed)) = words.get(2).and_then(|w| {
|
|
161
|
+
let name = raw(w, constants);
|
|
162
|
+
READERS.iter().find(|r| r.0.eq_ignore_ascii_case(&name))
|
|
163
|
+
}) else {
|
|
164
|
+
return Vec::new();
|
|
165
|
+
};
|
|
166
|
+
let value = |parameter: &str| {
|
|
167
|
+
words
|
|
168
|
+
.windows(3)
|
|
169
|
+
.find(|w| w[0].eq_ignore_ascii_case(parameter) && w[1] == "=")
|
|
170
|
+
.map(|w| raw(w[2], constants))
|
|
171
|
+
};
|
|
172
|
+
let applicant = reader == Reader::Personnel
|
|
173
|
+
&& value("tclas").is_some_and(|c| c.eq_ignore_ascii_case("B"));
|
|
174
|
+
let reader = if applicant { Reader::Applicant } else { reader };
|
|
175
|
+
fixed
|
|
176
|
+
.map(str::to_string)
|
|
177
|
+
.or_else(|| value("infty"))
|
|
178
|
+
.and_then(|n| table(&n, reader))
|
|
179
|
+
.map(read)
|
|
180
|
+
.into_iter()
|
|
181
|
+
.collect()
|
|
182
|
+
}
|
|
183
|
+
// RP-READ-INFOTYPE pernr infty table begda endda
|
|
184
|
+
K::MacroCall => match abap_syntax::sap_macro(file, s) {
|
|
185
|
+
Some(("RP-READ-INFOTYPE", used)) => arguments(file, s, used)
|
|
186
|
+
.get(1)
|
|
187
|
+
.and_then(|a| table(&number(&join(a), constants), Reader::Personnel))
|
|
188
|
+
.map(read)
|
|
189
|
+
.into_iter()
|
|
190
|
+
.collect(),
|
|
191
|
+
_ => Vec::new(),
|
|
192
|
+
},
|
|
193
|
+
_ => Vec::new(),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// What a macro of PNP reads and writes, by the position of its
|
|
198
|
+
/// arguments, counted from 0; the macros as SAP documents their use:
|
|
199
|
+
///
|
|
200
|
+
/// - `RP-PROVIDE-FROM-LAST table subtype begda endda` and
|
|
201
|
+
/// `RP-PROVIDE-FROM-FRST`: the header line of the infotype table gets the
|
|
202
|
+
/// last (first) record of the period, `pnp-sw-found` whether there is one
|
|
203
|
+
/// - `RP-READ-INFOTYPE pernr infty table begda endda`: the table gets the
|
|
204
|
+
/// records of the infotype of the period
|
|
205
|
+
/// - `RP-SET-DATA-INTERVAL infty begda endda`: the period the logical
|
|
206
|
+
/// database reads
|
|
207
|
+
pub(crate) struct MacroAccess {
|
|
208
|
+
pub reads: Vec<usize>,
|
|
209
|
+
/// (argument, whole)
|
|
210
|
+
pub writes: Vec<(usize, bool)>,
|
|
211
|
+
/// data the macro writes that is not one of its arguments
|
|
212
|
+
pub sets: &'static [&'static str],
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pub(crate) fn macro_access(name: &str) -> Option<MacroAccess> {
|
|
216
|
+
Some(match name {
|
|
217
|
+
"RP-PROVIDE-FROM-LAST" | "RP-PROVIDE-FROM-FRST" => MacroAccess {
|
|
218
|
+
reads: vec![0, 1, 2, 3],
|
|
219
|
+
writes: vec![(0, false)],
|
|
220
|
+
sets: &["pnp-sw-found"],
|
|
221
|
+
},
|
|
222
|
+
"RP-READ-INFOTYPE" => MacroAccess {
|
|
223
|
+
reads: vec![0, 3, 4],
|
|
224
|
+
writes: vec![(2, true)],
|
|
225
|
+
sets: &[],
|
|
226
|
+
},
|
|
227
|
+
"RP-SET-DATA-INTERVAL" => MacroAccess {
|
|
228
|
+
reads: vec![1, 2],
|
|
229
|
+
writes: Vec::new(),
|
|
230
|
+
sets: &[],
|
|
231
|
+
},
|
|
232
|
+
_ => return None,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// The arguments of a macro call after the `used` tokens of its name:
|
|
237
|
+
/// tokens joined without blanks, like `pn-begda`.
|
|
238
|
+
pub(crate) fn arguments<'a>(file: &'a File, s: &Statement, used: usize) -> Vec<Vec<&'a Token>> {
|
|
239
|
+
let mut out: Vec<Vec<&Token>> = Vec::new();
|
|
240
|
+
for t in s.tokens.iter().skip(used).map(|t| &file.tokens()[*t]) {
|
|
241
|
+
if t.kind == TokenKind::Punctuation {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
match out.last_mut() {
|
|
245
|
+
Some(arg) if !t.ws_before => arg.push(t),
|
|
246
|
+
_ => out.push(vec![t]),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pub(crate) fn join(tokens: &[&Token]) -> String {
|
|
253
|
+
tokens.iter().map(|t| t.text.as_str()).collect()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Whether a macro argument is data, not a literal, a number or `space`.
|
|
257
|
+
pub(crate) fn is_data(arg: &[&Token]) -> bool {
|
|
258
|
+
let first = arg[0];
|
|
259
|
+
first.kind == TokenKind::Identifier
|
|
260
|
+
&& !first.text.bytes().all(|b| b.is_ascii_digit())
|
|
261
|
+
&& !(arg.len() == 1 && first.is("SPACE"))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[cfg(test)]
|
|
265
|
+
mod tests {
|
|
266
|
+
use super::*;
|
|
267
|
+
|
|
268
|
+
fn reads(source: &str) -> Vec<String> {
|
|
269
|
+
let file = abap_syntax::parse(source);
|
|
270
|
+
let symbols = crate::symbols_of(&file);
|
|
271
|
+
let constants = crate::db::literal_constants(&symbols);
|
|
272
|
+
let declared = infotypes(&symbols);
|
|
273
|
+
file.statements
|
|
274
|
+
.iter()
|
|
275
|
+
.flat_map(|s| infotype_reads(&file, s, &constants, &declared))
|
|
276
|
+
.map(|t| t.name)
|
|
277
|
+
.collect()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn tables_by_number_range() {
|
|
282
|
+
let t = |n, r| table(n, r).unwrap();
|
|
283
|
+
assert_eq!(t("0001", Reader::Personnel), "PA0001");
|
|
284
|
+
assert_eq!(t("2001", Reader::Personnel), "PA2001");
|
|
285
|
+
assert_eq!(t("0002", Reader::Applicant), "PB0002");
|
|
286
|
+
assert_eq!(t("4000", Reader::Personnel), "PB4000");
|
|
287
|
+
// personnel planning, whoever reads it
|
|
288
|
+
assert_eq!(t("1001", Reader::Personnel), "HRP1001");
|
|
289
|
+
assert_eq!(t("5007", Reader::Personnel), "HRP5007");
|
|
290
|
+
// customer infotypes: the reader decides
|
|
291
|
+
assert_eq!(t("9001", Reader::Personnel), "PA9001");
|
|
292
|
+
assert_eq!(t("9001", Reader::Planning), "HRP9001");
|
|
293
|
+
assert_eq!(table("001", Reader::Personnel), None);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn infotype_tables() {
|
|
298
|
+
let got = reads(
|
|
299
|
+
"REPORT z.
|
|
300
|
+
NODES pernr.
|
|
301
|
+
INFOTYPES: 0001, 0002 MODE N, 1001, 0008 NAME lt_basic.
|
|
302
|
+
CONSTANTS c_addr TYPE infty VALUE '0006'.
|
|
303
|
+
GET pernr.
|
|
304
|
+
CALL FUNCTION 'HR_READ_INFOTYPE'
|
|
305
|
+
EXPORTING pernr = pernr-pernr infty = '0185'
|
|
306
|
+
TABLES infty_tab = lt_0185.
|
|
307
|
+
CALL FUNCTION 'HR_READ_INFOTYPE'
|
|
308
|
+
EXPORTING tclas = 'B' pernr = lv_appl infty = c_addr
|
|
309
|
+
TABLES infty_tab = lt_0006.
|
|
310
|
+
rp-read-infotype pernr-pernr 0007 p0007 pn-begda pn-endda.
|
|
311
|
+
CALL FUNCTION 'HR_READ_INFOTYPE' EXPORTING pernr = pernr-pernr infty = '5007'
|
|
312
|
+
TABLES infty_tab = lt_5007.
|
|
313
|
+
GET pernr LATE.",
|
|
314
|
+
);
|
|
315
|
+
assert_eq!(
|
|
316
|
+
got,
|
|
317
|
+
["PA0001", "PA0008", "PA0185", "PB0006", "PA0007", "HRP5007"]
|
|
318
|
+
);
|
|
319
|
+
let got = reads(
|
|
320
|
+
"REPORT z.
|
|
321
|
+
NODES objec.
|
|
322
|
+
INFOTYPES: 1000, 1001, 9010, 0001.
|
|
323
|
+
GET objec.
|
|
324
|
+
CALL FUNCTION 'RH_READ_INFTY'
|
|
325
|
+
EXPORTING plvar = '01' otype = 'S' objid = objec-objid infty = '9020'
|
|
326
|
+
TABLES innnn = lt_9020.
|
|
327
|
+
CALL FUNCTION 'RH_READ_INFTY_1001' EXPORTING plvar = '01'
|
|
328
|
+
TABLES i1001 = lt_1001.",
|
|
329
|
+
);
|
|
330
|
+
// PCH reads only planning infotypes; customer ones are of planning there
|
|
331
|
+
assert_eq!(got, ["HRP1000", "HRP1001", "HRP9010", "HRP9020", "HRP1001"]);
|
|
332
|
+
}
|
|
333
|
+
}
|