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.
Files changed (74) hide show
  1. {abap_parser-0.2.0 → abap_parser-0.4.0}/Cargo.lock +10 -9
  2. {abap_parser-0.2.0 → abap_parser-0.4.0}/Cargo.toml +1 -1
  3. {abap_parser-0.2.0 → abap_parser-0.4.0}/PKG-INFO +4 -1
  4. {abap_parser-0.2.0 → abap_parser-0.4.0}/README.md +3 -0
  5. {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/README.md +3 -0
  6. {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/src/lib.rs +39 -5
  7. {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/tests/test_symbols.py +28 -0
  8. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/calls.rs +56 -0
  9. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/db.rs +5 -0
  10. abap_parser-0.4.0/crates/abap-analysis/src/enhance.rs +305 -0
  11. abap_parser-0.4.0/crates/abap-analysis/src/hr.rs +333 -0
  12. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/lib.rs +96 -32
  13. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/logic.rs +655 -64
  14. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/stores.rs +54 -12
  15. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/symbols.rs +42 -0
  16. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/text.rs +49 -5
  17. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/workspace.rs +752 -85
  18. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/adt_mock.rs +75 -1
  19. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/call_graph_corpus.rs +3 -2
  20. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/symbols_diff.rs +3 -2
  21. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-lexer/src/lib.rs +12 -5
  22. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-model/src/lib.rs +114 -2
  23. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/Cargo.toml +1 -0
  24. abap_parser-0.4.0/crates/abap-source/src/adt.rs +1629 -0
  25. abap_parser-0.4.0/crates/abap-source/src/badi.rs +223 -0
  26. abap_parser-0.4.0/crates/abap-source/src/cache.rs +281 -0
  27. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/fs.rs +145 -6
  28. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/lib.rs +219 -1
  29. abap_parser-0.4.0/crates/abap-source/src/mock/tables.rs +535 -0
  30. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/mock.rs +362 -2
  31. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-source/src/options.rs +13 -1
  32. abap_parser-0.4.0/crates/abap-sql/src/cluster.rs +159 -0
  33. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/lib.rs +5 -1
  34. abap_parser-0.4.0/crates/abap-syntax/src/check.rs +479 -0
  35. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/classify.rs +26 -0
  36. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/lib.rs +73 -4
  37. abap_parser-0.4.0/crates/abap-syntax/src/release.rs +1418 -0
  38. abap_parser-0.4.0/crates/abap-syntax/src/system.rs +276 -0
  39. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/abaplint_statements.rs +15 -2
  40. abap_parser-0.4.0/crates/abap-syntax/tests/release_diff.rs +295 -0
  41. abap_parser-0.4.0/crates/abap-syntax/tests/syntax_errors.rs +162 -0
  42. {abap_parser-0.2.0 → abap_parser-0.4.0}/python/abap_parser/__init__.py +28 -6
  43. {abap_parser-0.2.0 → abap_parser-0.4.0}/python/abap_parser/types.py +73 -11
  44. abap_parser-0.2.0/crates/abap-source/src/adt.rs +0 -727
  45. abap_parser-0.2.0/crates/abap-source/src/cache.rs +0 -126
  46. {abap_parser-0.2.0 → abap_parser-0.4.0}/LICENSE +0 -0
  47. {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/Cargo.toml +0 -0
  48. {abap_parser-0.2.0 → abap_parser-0.4.0}/bindings/python/LICENSE +0 -0
  49. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/Cargo.toml +0 -0
  50. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/args.rs +0 -0
  51. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/context.rs +0 -0
  52. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/src/ddic.rs +0 -0
  53. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
  54. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
  55. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/logic_corpus.rs +0 -0
  56. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-lexer/Cargo.toml +0 -0
  57. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-model/Cargo.toml +0 -0
  58. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/Cargo.toml +0 -0
  59. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/cds.rs +0 -0
  60. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/hana.rs +0 -0
  61. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/src/tests.rs +0 -0
  62. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/tests/cds_corpus.rs +0 -0
  63. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
  64. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/Cargo.toml +0 -0
  65. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/decl.rs +0 -0
  66. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/expr.rs +0 -0
  67. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/kind.rs +0 -0
  68. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/operands.rs +0 -0
  69. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/src/structure.rs +0 -0
  70. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
  71. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
  72. {abap_parser-0.2.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
  73. {abap_parser-0.2.0 → abap_parser-0.4.0}/pyproject.toml +0 -0
  74. {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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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.2.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
- py.detach(|| abap_analysis::json::call_graph(&roots, options, depth))
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
- fn fetch_source(py: Python<'_>, reference: &str, options: &str) -> PyResult<Option<String>> {
39
- py.detach(|| abap_analysis::json::fetch(reference, options))
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
- py.detach(|| abap_analysis::json::logic_object(reference, options, depth))
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
- py.detach(|| abap_analysis::json::object_text(analysis, &objects, options, depth))
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
+ }