abap-parser 0.2.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.2.0 → abap_parser-0.4.0}/Cargo.lock +10 -9
- {abap_parser-0.2.0 → abap_parser-0.4.0}/Cargo.toml +1 -1
- {abap_parser-0.2.0 → abap_parser-0.4.0}/PKG-INFO +4 -1
- {abap_parser-0.2.0 → abap_parser-0.4.0}/README.md +3 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/README.md +3 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/src/lib.rs +39 -5
- {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/tests/test_symbols.py +28 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/calls.rs +56 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/db.rs +5 -0
- abap_parser-0.4.0/crates/abap-analysis/src/enhance.rs +305 -0
- abap_parser-0.4.0/crates/abap-analysis/src/hr.rs +333 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/lib.rs +96 -32
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/logic.rs +655 -64
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/stores.rs +54 -12
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/symbols.rs +42 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/text.rs +49 -5
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/workspace.rs +752 -85
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/adt_mock.rs +75 -1
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/call_graph_corpus.rs +3 -2
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/symbols_diff.rs +3 -2
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-lexer/src/lib.rs +12 -5
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-model/src/lib.rs +114 -2
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/Cargo.toml +1 -0
- abap_parser-0.4.0/crates/abap-source/src/adt.rs +1629 -0
- abap_parser-0.4.0/crates/abap-source/src/badi.rs +223 -0
- abap_parser-0.4.0/crates/abap-source/src/cache.rs +281 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/fs.rs +145 -6
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/lib.rs +219 -1
- abap_parser-0.4.0/crates/abap-source/src/mock/tables.rs +535 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/mock.rs +362 -2
- {abap_parser-0.2.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.2.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.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/classify.rs +26 -0
- {abap_parser-0.2.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.2.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.2.0 → abap_parser-0.4.0}/python/abap_parser/__init__.py +28 -6
- {abap_parser-0.2.0 → abap_parser-0.4.0}/python/abap_parser/types.py +73 -11
- abap_parser-0.2.0/crates/abap-source/src/adt.rs +0 -727
- abap_parser-0.2.0/crates/abap-source/src/cache.rs +0 -126
- {abap_parser-0.2.0 → abap_parser-0.4.0}/LICENSE +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/LICENSE +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/args.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/context.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/ddic.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/logic_corpus.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-lexer/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-model/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/cds.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/hana.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/tests.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/tests/cds_corpus.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/Cargo.toml +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/decl.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/expr.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/kind.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/operands.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/structure.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
- {abap_parser-0.2.0 → abap_parser-0.4.0}/pyproject.toml +0 -0
- {abap_parser-0.2.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,16 +63,17 @@ 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",
|
|
70
|
+
"serde_json",
|
|
70
71
|
"ureq",
|
|
71
72
|
]
|
|
72
73
|
|
|
73
74
|
[[package]]
|
|
74
75
|
name = "abap-sql"
|
|
75
|
-
version = "0.
|
|
76
|
+
version = "0.4.0"
|
|
76
77
|
dependencies = [
|
|
77
78
|
"abap-model",
|
|
78
79
|
"abap-syntax",
|
|
@@ -82,7 +83,7 @@ dependencies = [
|
|
|
82
83
|
|
|
83
84
|
[[package]]
|
|
84
85
|
name = "abap-syntax"
|
|
85
|
-
version = "0.
|
|
86
|
+
version = "0.4.0"
|
|
86
87
|
dependencies = [
|
|
87
88
|
"abap-lexer",
|
|
88
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"
|
|
@@ -402,6 +402,18 @@ pub(crate) struct CallExtractor<'a> {
|
|
|
402
402
|
pub file: &'a File,
|
|
403
403
|
pub symbols: &'a m::FileSymbols,
|
|
404
404
|
pub resolver: &'a dyn TypeResolver,
|
|
405
|
+
/// main program, for the names of customer exits
|
|
406
|
+
pub program: Option<String>,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// The main program named by `REPORT`, `PROGRAM` or `FUNCTION-POOL`.
|
|
410
|
+
pub(crate) fn main_program(symbols: &m::FileSymbols) -> Option<String> {
|
|
411
|
+
let p = symbols.program.as_ref()?;
|
|
412
|
+
match p.kind.as_str() {
|
|
413
|
+
"report" | "program" => Some(p.name.to_ascii_uppercase()),
|
|
414
|
+
"function-pool" => Some(format!("SAPL{}", p.name.to_ascii_uppercase())),
|
|
415
|
+
_ => None,
|
|
416
|
+
}
|
|
405
417
|
}
|
|
406
418
|
|
|
407
419
|
impl<'a> CallExtractor<'a> {
|
|
@@ -711,6 +723,25 @@ impl<'a> CallExtractor<'a> {
|
|
|
711
723
|
out.push(new_call(kind, name, dynamic, opts));
|
|
712
724
|
};
|
|
713
725
|
match s.kind {
|
|
726
|
+
K::CallFunction if word(1) == "CUSTOMER" => {
|
|
727
|
+
// CALL CUSTOMER-FUNCTION 'nnn': the exit EXIT_<main program>_nnn
|
|
728
|
+
let Some(k) = (2..t.len()).find(|k| literal(t[*k]).is_some()) else {
|
|
729
|
+
return t.len();
|
|
730
|
+
};
|
|
731
|
+
let number = literal(t[k]).unwrap_or_default();
|
|
732
|
+
let opts = vec!["CUSTOMER-FUNCTION".to_string()];
|
|
733
|
+
match &self.program {
|
|
734
|
+
Some(p) => push(
|
|
735
|
+
m::CallKind::Function,
|
|
736
|
+
format!("EXIT_{}_{number}", p.to_ascii_uppercase()),
|
|
737
|
+
false,
|
|
738
|
+
opts,
|
|
739
|
+
),
|
|
740
|
+
// the program is not known
|
|
741
|
+
None => push(m::CallKind::Function, text(k, k + 1), true, opts),
|
|
742
|
+
}
|
|
743
|
+
k + 1
|
|
744
|
+
}
|
|
714
745
|
K::CallFunction => {
|
|
715
746
|
let (name, dynamic) = target(2);
|
|
716
747
|
let opts = options(&[
|
|
@@ -1178,6 +1209,7 @@ pub(crate) fn new_call(
|
|
|
1178
1209
|
from: None,
|
|
1179
1210
|
name,
|
|
1180
1211
|
class: None,
|
|
1212
|
+
badi: None,
|
|
1181
1213
|
program: None,
|
|
1182
1214
|
receiver: None,
|
|
1183
1215
|
is_static: false,
|
|
@@ -1379,6 +1411,7 @@ pub fn calls(source: &str) -> Vec<m::Call> {
|
|
|
1379
1411
|
file: &file,
|
|
1380
1412
|
symbols: &symbols,
|
|
1381
1413
|
resolver: &resolver,
|
|
1414
|
+
program: main_program(&symbols),
|
|
1382
1415
|
}
|
|
1383
1416
|
.calls()
|
|
1384
1417
|
}
|
|
@@ -1431,6 +1464,29 @@ CLASS lcl_main IMPLEMENTATION.
|
|
|
1431
1464
|
ENDCLASS.
|
|
1432
1465
|
"#;
|
|
1433
1466
|
|
|
1467
|
+
#[test]
|
|
1468
|
+
fn customer_exits_are_named_by_the_program() {
|
|
1469
|
+
let names = |src: &str| -> Vec<(String, bool)> {
|
|
1470
|
+
calls(src)
|
|
1471
|
+
.into_iter()
|
|
1472
|
+
.map(|c| (c.name, c.dynamic))
|
|
1473
|
+
.collect()
|
|
1474
|
+
};
|
|
1475
|
+
assert_eq!(
|
|
1476
|
+
names("REPORT sapmv45a.\nCALL CUSTOMER-FUNCTION '001' EXPORTING iv = 1."),
|
|
1477
|
+
[("EXIT_SAPMV45A_001".to_string(), false)]
|
|
1478
|
+
);
|
|
1479
|
+
assert_eq!(
|
|
1480
|
+
names("FUNCTION-POOL v45a.\nCALL CUSTOMER-FUNCTION '010'."),
|
|
1481
|
+
[("EXIT_SAPLV45A_010".to_string(), false)]
|
|
1482
|
+
);
|
|
1483
|
+
// an include alone does not tell its program
|
|
1484
|
+
assert_eq!(
|
|
1485
|
+
names("CALL CUSTOMER-FUNCTION '002'."),
|
|
1486
|
+
[("'002'".to_string(), true)]
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1434
1490
|
#[test]
|
|
1435
1491
|
fn calls_with_types() {
|
|
1436
1492
|
let c = calls(SRC);
|
|
@@ -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,305 @@
|
|
|
1
|
+
//! Code of enhancement implementations put where it runs: after an
|
|
2
|
+
//! `ENHANCEMENT-POINT`, in place of an `ENHANCEMENT-SECTION`, or at the begin
|
|
3
|
+
//! or end of a form, function module, method or include.
|
|
4
|
+
//!
|
|
5
|
+
//! The code goes into the source on one line, the line of its position, so
|
|
6
|
+
//! the rows of the rest of the source stay as they are.
|
|
7
|
+
|
|
8
|
+
use abap_source::Hook;
|
|
9
|
+
use abap_syntax::{File, StatementKind as K, Token, TokenKind};
|
|
10
|
+
|
|
11
|
+
/// Where in a source the code of a hook goes.
|
|
12
|
+
struct Edit {
|
|
13
|
+
/// byte offset the code is inserted at
|
|
14
|
+
at: usize,
|
|
15
|
+
/// bytes from `at` on that the code replaces, for sections
|
|
16
|
+
replaces: usize,
|
|
17
|
+
code: String,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// The sources with the hooks put in, for the units that change, and the
|
|
21
|
+
/// hooks that found their position.
|
|
22
|
+
pub(crate) fn apply<'a>(
|
|
23
|
+
units: &[(&str, &File)],
|
|
24
|
+
hooks: &'a [Hook],
|
|
25
|
+
) -> (Vec<(usize, String)>, Vec<&'a Hook>) {
|
|
26
|
+
let mut edits: Vec<Vec<Edit>> = units.iter().map(|_| Vec::new()).collect();
|
|
27
|
+
let mut applied = Vec::new();
|
|
28
|
+
for h in hooks {
|
|
29
|
+
let Some((u, at, replaces)) = position(units, h) else {
|
|
30
|
+
continue;
|
|
31
|
+
};
|
|
32
|
+
edits[u].push(Edit {
|
|
33
|
+
at,
|
|
34
|
+
replaces,
|
|
35
|
+
code: one_line(&h.code),
|
|
36
|
+
});
|
|
37
|
+
applied.push(h);
|
|
38
|
+
}
|
|
39
|
+
let mut out = Vec::new();
|
|
40
|
+
for (u, mut edits) in edits.into_iter().enumerate() {
|
|
41
|
+
if edits.is_empty() {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
let mut text = units[u].1.lexed.source.clone();
|
|
45
|
+
// from the end, so earlier offsets stay valid
|
|
46
|
+
edits.sort_by_key(|e| std::cmp::Reverse(e.at));
|
|
47
|
+
for e in edits {
|
|
48
|
+
let end = e.at + e.replaces;
|
|
49
|
+
// replaced code keeps its line breaks, so rows stay
|
|
50
|
+
let kept: String = text[e.at..end]
|
|
51
|
+
.chars()
|
|
52
|
+
.map(|c| if c == '\n' { '\n' } else { ' ' })
|
|
53
|
+
.collect();
|
|
54
|
+
text.replace_range(e.at..end, &format!(" {} {kept}", e.code));
|
|
55
|
+
}
|
|
56
|
+
out.push((u, text));
|
|
57
|
+
}
|
|
58
|
+
(out, applied)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// The code of a hook on one line, without comments.
|
|
62
|
+
fn one_line(code: &str) -> String {
|
|
63
|
+
let lexed = abap_syntax::lex(code);
|
|
64
|
+
let tokens: Vec<&Token> = lexed
|
|
65
|
+
.tokens
|
|
66
|
+
.iter()
|
|
67
|
+
.filter(|t| t.kind != TokenKind::Comment)
|
|
68
|
+
.collect();
|
|
69
|
+
abap_syntax::join_tokens(tokens)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// The unit, byte offset and length of replaced code for a hook.
|
|
73
|
+
fn position(units: &[(&str, &File)], h: &Hook) -> Option<(usize, usize, usize)> {
|
|
74
|
+
// enhancements of a function module's parameters have two sides
|
|
75
|
+
if h.position.matches("\\SE:").count() > 1 {
|
|
76
|
+
return None;
|
|
77
|
+
}
|
|
78
|
+
let include = h.part("IC");
|
|
79
|
+
let in_unit = |u: usize| include.as_deref().is_none_or(|i| units[u].0 == i);
|
|
80
|
+
if let Some(point) = h.part("EX") {
|
|
81
|
+
for (u, (_, file)) in units.iter().enumerate().filter(|(u, _)| in_unit(*u)) {
|
|
82
|
+
for (i, s) in file.statements.iter().enumerate() {
|
|
83
|
+
// ENHANCEMENT-POINT name SPOTS ...
|
|
84
|
+
let spots = s.tokens.iter().position(|k| file.tokens()[*k].is("SPOTS"));
|
|
85
|
+
let named = spots
|
|
86
|
+
.and_then(|k| k.checked_sub(1))
|
|
87
|
+
.and_then(|k| word(file, s, k))
|
|
88
|
+
.is_some_and(|w| w == point);
|
|
89
|
+
match s.kind {
|
|
90
|
+
K::EnhancementPoint if named => return Some((u, end(file, i), 0)),
|
|
91
|
+
K::EnhancementSection if named => {
|
|
92
|
+
// the implementation replaces the code of the section
|
|
93
|
+
let close = (i + 1..file.statements.len())
|
|
94
|
+
.find(|k| file.statements[*k].kind == K::EndEnhancementSection)?;
|
|
95
|
+
let from = end(file, i);
|
|
96
|
+
return Some((u, from, start(file, close) - from));
|
|
97
|
+
}
|
|
98
|
+
_ => {}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return None;
|
|
103
|
+
}
|
|
104
|
+
let begin = match h.part("SE")?.as_str() {
|
|
105
|
+
"BEGIN" => true,
|
|
106
|
+
"END" => false,
|
|
107
|
+
_ => return None,
|
|
108
|
+
};
|
|
109
|
+
let block = if let Some(form) = h.part("FO") {
|
|
110
|
+
Some((K::Form, K::EndForm, form, None))
|
|
111
|
+
} else if let Some(function) = h.part("FU") {
|
|
112
|
+
Some((K::FunctionModule, K::EndFunction, function, None))
|
|
113
|
+
} else {
|
|
114
|
+
h.part("ME")
|
|
115
|
+
.map(|method| (K::MethodImplementation, K::EndMethod, method, h.part("TY")))
|
|
116
|
+
};
|
|
117
|
+
let Some((open, close, name, class)) = block else {
|
|
118
|
+
// the begin or end of an include
|
|
119
|
+
let include = include?;
|
|
120
|
+
let u = units.iter().position(|(n, _)| *n == include)?;
|
|
121
|
+
let file = units[u].1;
|
|
122
|
+
return Some(if begin {
|
|
123
|
+
(u, 0, 0)
|
|
124
|
+
} else {
|
|
125
|
+
let last = (0..file.statements.len()).rev().find(|i| end(file, *i) > 0);
|
|
126
|
+
(u, last.map_or(0, |i| end(file, i)), 0)
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
for (u, (_, file)) in units.iter().enumerate().filter(|(u, _)| in_unit(*u)) {
|
|
130
|
+
let mut in_class: Option<String> = None;
|
|
131
|
+
for (i, s) in file.statements.iter().enumerate() {
|
|
132
|
+
if s.kind == K::ClassImplementation {
|
|
133
|
+
in_class = word(file, s, 1);
|
|
134
|
+
}
|
|
135
|
+
let found = s.kind == open
|
|
136
|
+
&& word(file, s, 1).is_some_and(|w| w == name)
|
|
137
|
+
&& class
|
|
138
|
+
.as_deref()
|
|
139
|
+
.is_none_or(|c| in_class.as_deref() == Some(c));
|
|
140
|
+
if !found {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if begin {
|
|
144
|
+
return Some((u, end(file, i), 0));
|
|
145
|
+
}
|
|
146
|
+
let k = (i + 1..file.statements.len()).find(|k| file.statements[*k].kind == close)?;
|
|
147
|
+
return Some((u, start(file, k), 0));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
None
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The `n`th word of a statement, upper case.
|
|
154
|
+
fn word(file: &File, s: &abap_syntax::Statement, n: usize) -> Option<String> {
|
|
155
|
+
s.tokens.get(n).map(|k| file.tokens()[*k].upper())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn start(file: &File, i: usize) -> usize {
|
|
159
|
+
let s = &file.statements[i];
|
|
160
|
+
s.tokens.first().map_or(0, |k| file.tokens()[*k].span.start)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The end of the statement `i`, without comments; 0 for comments only.
|
|
164
|
+
fn end(file: &File, i: usize) -> usize {
|
|
165
|
+
let s = &file.statements[i];
|
|
166
|
+
s.tokens
|
|
167
|
+
.iter()
|
|
168
|
+
.map(|k| &file.tokens()[*k])
|
|
169
|
+
.filter(|t| t.kind != TokenKind::Comment)
|
|
170
|
+
.map(|t| t.span.end)
|
|
171
|
+
.max()
|
|
172
|
+
.unwrap_or(0)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[cfg(test)]
|
|
176
|
+
mod tests {
|
|
177
|
+
use super::*;
|
|
178
|
+
|
|
179
|
+
fn hook(position: &str, code: &str) -> Hook {
|
|
180
|
+
Hook {
|
|
181
|
+
enhancement: "ZENH".into(),
|
|
182
|
+
position: position.into(),
|
|
183
|
+
code: code.into(),
|
|
184
|
+
origin: String::new(),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn applied(source: &str, unit: &str, hooks: &[Hook]) -> String {
|
|
189
|
+
let file = abap_syntax::parse(source);
|
|
190
|
+
let (texts, _) = apply(&[(unit, &file)], hooks);
|
|
191
|
+
texts.into_iter().next().map(|(_, t)| t).unwrap_or_default()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn hooks_go_where_they_run() {
|
|
196
|
+
let src = "REPORT zr.
|
|
197
|
+
ENHANCEMENT-POINT ep_top SPOTS zspot STATIC.
|
|
198
|
+
FORM save.
|
|
199
|
+
gv = 1.
|
|
200
|
+
ENDFORM.
|
|
201
|
+
ENHANCEMENT-SECTION es_calc SPOTS zspot.
|
|
202
|
+
gv = 2.
|
|
203
|
+
END-ENHANCEMENT-SECTION.";
|
|
204
|
+
let text = applied(
|
|
205
|
+
src,
|
|
206
|
+
"ZR",
|
|
207
|
+
&[
|
|
208
|
+
hook(
|
|
209
|
+
"\\PR:ZR\\EX:EP_TOP\\EI",
|
|
210
|
+
"ENHANCEMENT 1 ZENH.\n DATA gv_x TYPE i. \" new\nENDENHANCEMENT.",
|
|
211
|
+
),
|
|
212
|
+
hook(
|
|
213
|
+
"\\PR:ZR\\FO:SAVE\\SE:END\\EI",
|
|
214
|
+
"ENHANCEMENT 2 ZENH.\n gv = 3.\nENDENHANCEMENT.",
|
|
215
|
+
),
|
|
216
|
+
hook(
|
|
217
|
+
"\\PR:ZR\\FO:SAVE\\SE:BEGIN\\EI",
|
|
218
|
+
"ENHANCEMENT 3 ZENH.\n gv = 4.\nENDENHANCEMENT.",
|
|
219
|
+
),
|
|
220
|
+
hook(
|
|
221
|
+
"\\PR:ZR\\EX:ES_CALC\\EI",
|
|
222
|
+
"ENHANCEMENT 4 ZENH.\n gv = 5.\nENDENHANCEMENT.",
|
|
223
|
+
),
|
|
224
|
+
hook(
|
|
225
|
+
"\\PR:ZR\\FO:NONE\\SE:END\\EI",
|
|
226
|
+
"ENHANCEMENT 5 ZENH.\nENDENHANCEMENT.",
|
|
227
|
+
),
|
|
228
|
+
],
|
|
229
|
+
);
|
|
230
|
+
let lines: Vec<&str> = text.lines().map(str::trim_end).collect();
|
|
231
|
+
assert_eq!(
|
|
232
|
+
lines,
|
|
233
|
+
[
|
|
234
|
+
"REPORT zr.",
|
|
235
|
+
"ENHANCEMENT-POINT ep_top SPOTS zspot STATIC. ENHANCEMENT 1 ZENH. DATA gv_x TYPE i. ENDENHANCEMENT.",
|
|
236
|
+
"FORM save. ENHANCEMENT 3 ZENH. gv = 4. ENDENHANCEMENT.",
|
|
237
|
+
" gv = 1.",
|
|
238
|
+
" ENHANCEMENT 2 ZENH. gv = 3. ENDENHANCEMENT. ENDFORM.",
|
|
239
|
+
"ENHANCEMENT-SECTION es_calc SPOTS zspot. ENHANCEMENT 4 ZENH. gv = 5. ENDENHANCEMENT.",
|
|
240
|
+
"",
|
|
241
|
+
"END-ENHANCEMENT-SECTION.",
|
|
242
|
+
]
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn methods_function_modules_and_includes() {
|
|
248
|
+
let src = "CLASS lcl IMPLEMENTATION.
|
|
249
|
+
METHOD run.
|
|
250
|
+
ENDMETHOD.
|
|
251
|
+
ENDCLASS.
|
|
252
|
+
CLASS zcl_x IMPLEMENTATION.
|
|
253
|
+
METHOD run.
|
|
254
|
+
ENDMETHOD.
|
|
255
|
+
ENDCLASS.";
|
|
256
|
+
let text = applied(
|
|
257
|
+
src,
|
|
258
|
+
"ZCL_X=========================CP",
|
|
259
|
+
&[hook(
|
|
260
|
+
"\\TY:ZCL_X\\ME:RUN\\SE:BEGIN\\EI",
|
|
261
|
+
"ENHANCEMENT 1 ZENH.\nx = 1.\nENDENHANCEMENT.",
|
|
262
|
+
)],
|
|
263
|
+
);
|
|
264
|
+
assert_eq!(
|
|
265
|
+
text.lines().nth(5).unwrap(),
|
|
266
|
+
" METHOD run. ENHANCEMENT 1 ZENH. x = 1. ENDENHANCEMENT. "
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
let text = applied(
|
|
270
|
+
"FUNCTION z_fm.\nENDFUNCTION.",
|
|
271
|
+
"Z_FM",
|
|
272
|
+
&[hook(
|
|
273
|
+
"\\FU:Z_FM\\SE:BEGIN\\EI",
|
|
274
|
+
"ENHANCEMENT 1 ZENH.\nx = 1.\nENDENHANCEMENT.",
|
|
275
|
+
)],
|
|
276
|
+
);
|
|
277
|
+
assert_eq!(
|
|
278
|
+
text.lines().next().unwrap(),
|
|
279
|
+
"FUNCTION z_fm. ENHANCEMENT 1 ZENH. x = 1. ENDENHANCEMENT. "
|
|
280
|
+
);
|
|
281
|
+
// parameters of function modules are not code
|
|
282
|
+
let text = applied(
|
|
283
|
+
"FUNCTION z_fm.\nENDFUNCTION.",
|
|
284
|
+
"Z_FM",
|
|
285
|
+
&[hook(
|
|
286
|
+
"\\FU:Z_FM\\SE:IMPORTING\\SE:END\\EI",
|
|
287
|
+
"ENHANCEMENT 1 ZENH.\nENDENHANCEMENT.",
|
|
288
|
+
)],
|
|
289
|
+
);
|
|
290
|
+
assert_eq!(text, "");
|
|
291
|
+
|
|
292
|
+
let text = applied(
|
|
293
|
+
"x = 1.\n\" end",
|
|
294
|
+
"ZINC",
|
|
295
|
+
&[hook(
|
|
296
|
+
"\\PR:ZR\\IC:ZINC\\SE:END\\EI",
|
|
297
|
+
"ENHANCEMENT 1 ZENH.\ny = 2.\nENDENHANCEMENT.",
|
|
298
|
+
)],
|
|
299
|
+
);
|
|
300
|
+
assert_eq!(
|
|
301
|
+
text,
|
|
302
|
+
"x = 1. ENHANCEMENT 1 ZENH. y = 2. ENDENHANCEMENT. \n\" end"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|