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.
Files changed (72) hide show
  1. {abap_parser-0.3.0 → abap_parser-0.4.0}/Cargo.lock +9 -9
  2. {abap_parser-0.3.0 → abap_parser-0.4.0}/Cargo.toml +1 -1
  3. {abap_parser-0.3.0 → abap_parser-0.4.0}/PKG-INFO +4 -1
  4. {abap_parser-0.3.0 → abap_parser-0.4.0}/README.md +3 -0
  5. {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/README.md +3 -0
  6. {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/src/lib.rs +39 -5
  7. {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/tests/test_symbols.py +28 -0
  8. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/db.rs +5 -0
  9. abap_parser-0.4.0/crates/abap-analysis/src/hr.rs +333 -0
  10. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/lib.rs +95 -32
  11. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/logic.rs +643 -62
  12. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/stores.rs +54 -12
  13. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/symbols.rs +42 -0
  14. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/text.rs +38 -3
  15. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/workspace.rs +48 -0
  16. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/adt_mock.rs +75 -1
  17. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/call_graph_corpus.rs +3 -2
  18. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/symbols_diff.rs +3 -2
  19. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-lexer/src/lib.rs +12 -5
  20. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-model/src/lib.rs +110 -2
  21. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/adt.rs +630 -81
  22. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/badi.rs +6 -1
  23. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/cache.rs +16 -3
  24. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/fs.rs +6 -6
  25. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/lib.rs +62 -1
  26. abap_parser-0.4.0/crates/abap-source/src/mock/tables.rs +535 -0
  27. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/mock.rs +362 -2
  28. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/src/options.rs +13 -1
  29. abap_parser-0.4.0/crates/abap-sql/src/cluster.rs +159 -0
  30. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/lib.rs +5 -1
  31. abap_parser-0.4.0/crates/abap-syntax/src/check.rs +479 -0
  32. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/classify.rs +26 -0
  33. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/lib.rs +73 -4
  34. abap_parser-0.4.0/crates/abap-syntax/src/release.rs +1418 -0
  35. abap_parser-0.4.0/crates/abap-syntax/src/system.rs +276 -0
  36. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/abaplint_statements.rs +15 -2
  37. abap_parser-0.4.0/crates/abap-syntax/tests/release_diff.rs +295 -0
  38. abap_parser-0.4.0/crates/abap-syntax/tests/syntax_errors.rs +162 -0
  39. {abap_parser-0.3.0 → abap_parser-0.4.0}/python/abap_parser/__init__.py +28 -6
  40. {abap_parser-0.3.0 → abap_parser-0.4.0}/python/abap_parser/types.py +72 -10
  41. {abap_parser-0.3.0 → abap_parser-0.4.0}/LICENSE +0 -0
  42. {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/Cargo.toml +0 -0
  43. {abap_parser-0.3.0 → abap_parser-0.4.0}/bindings/python/LICENSE +0 -0
  44. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/Cargo.toml +0 -0
  45. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/args.rs +0 -0
  46. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/calls.rs +0 -0
  47. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/context.rs +0 -0
  48. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/ddic.rs +0 -0
  49. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/src/enhance.rs +0 -0
  50. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
  51. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
  52. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-analysis/tests/logic_corpus.rs +0 -0
  53. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-lexer/Cargo.toml +0 -0
  54. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-model/Cargo.toml +0 -0
  55. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-source/Cargo.toml +0 -0
  56. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/Cargo.toml +0 -0
  57. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/cds.rs +0 -0
  58. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/hana.rs +0 -0
  59. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/src/tests.rs +0 -0
  60. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/tests/cds_corpus.rs +0 -0
  61. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
  62. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/Cargo.toml +0 -0
  63. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/decl.rs +0 -0
  64. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/expr.rs +0 -0
  65. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/kind.rs +0 -0
  66. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/operands.rs +0 -0
  67. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/src/structure.rs +0 -0
  68. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
  69. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
  70. {abap_parser-0.3.0 → abap_parser-0.4.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
  71. {abap_parser-0.3.0 → abap_parser-0.4.0}/pyproject.toml +0 -0
  72. {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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.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"
@@ -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
+ }