abap-parser 0.1.0__tar.gz → 0.2.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 (62) hide show
  1. {abap_parser-0.1.0 → abap_parser-0.2.0}/Cargo.lock +9 -9
  2. {abap_parser-0.1.0 → abap_parser-0.2.0}/Cargo.toml +1 -1
  3. {abap_parser-0.1.0 → abap_parser-0.2.0}/PKG-INFO +8 -3
  4. {abap_parser-0.1.0 → abap_parser-0.2.0}/README.md +7 -2
  5. {abap_parser-0.1.0 → abap_parser-0.2.0}/bindings/python/README.md +7 -2
  6. {abap_parser-0.1.0 → abap_parser-0.2.0}/bindings/python/src/lib.rs +23 -0
  7. {abap_parser-0.1.0 → abap_parser-0.2.0}/bindings/python/tests/test_symbols.py +17 -0
  8. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/calls.rs +188 -46
  9. abap_parser-0.2.0/crates/abap-analysis/src/ddic.rs +202 -0
  10. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/lib.rs +65 -0
  11. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/logic.rs +382 -13
  12. abap_parser-0.2.0/crates/abap-analysis/src/stores.rs +212 -0
  13. abap_parser-0.2.0/crates/abap-analysis/src/text.rs +805 -0
  14. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/workspace.rs +395 -8
  15. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/logic_corpus.rs +16 -2
  16. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-model/src/lib.rs +128 -4
  17. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/adt.rs +186 -8
  18. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/cache.rs +14 -2
  19. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/fs.rs +130 -34
  20. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/lib.rs +72 -2
  21. abap_parser-0.2.0/crates/abap-sql/src/cds.rs +995 -0
  22. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-sql/src/lib.rs +1 -0
  23. abap_parser-0.2.0/crates/abap-sql/tests/cds_corpus.rs +59 -0
  24. {abap_parser-0.1.0 → abap_parser-0.2.0}/python/abap_parser/__init__.py +40 -1
  25. {abap_parser-0.1.0 → abap_parser-0.2.0}/python/abap_parser/types.py +90 -5
  26. {abap_parser-0.1.0 → abap_parser-0.2.0}/LICENSE +0 -0
  27. {abap_parser-0.1.0 → abap_parser-0.2.0}/bindings/python/Cargo.toml +0 -0
  28. {abap_parser-0.1.0 → abap_parser-0.2.0}/bindings/python/LICENSE +0 -0
  29. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/Cargo.toml +0 -0
  30. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/args.rs +0 -0
  31. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/context.rs +0 -0
  32. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/db.rs +0 -0
  33. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/src/symbols.rs +0 -0
  34. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/adt_mock.rs +0 -0
  35. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
  36. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/call_graph_corpus.rs +0 -0
  37. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
  38. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-analysis/tests/symbols_diff.rs +0 -0
  39. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-lexer/Cargo.toml +0 -0
  40. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-lexer/src/lib.rs +0 -0
  41. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-model/Cargo.toml +0 -0
  42. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/Cargo.toml +0 -0
  43. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/mock.rs +0 -0
  44. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-source/src/options.rs +0 -0
  45. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-sql/Cargo.toml +0 -0
  46. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-sql/src/hana.rs +0 -0
  47. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-sql/src/tests.rs +0 -0
  48. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
  49. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/Cargo.toml +0 -0
  50. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/classify.rs +0 -0
  51. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/decl.rs +0 -0
  52. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/expr.rs +0 -0
  53. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/kind.rs +0 -0
  54. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/lib.rs +0 -0
  55. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/operands.rs +0 -0
  56. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/src/structure.rs +0 -0
  57. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/tests/abaplint_statements.rs +0 -0
  58. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
  59. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
  60. {abap_parser-0.1.0 → abap_parser-0.2.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
  61. {abap_parser-0.1.0 → abap_parser-0.2.0}/pyproject.toml +0 -0
  62. {abap_parser-0.1.0 → abap_parser-0.2.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.1.0"
7
+ version = "0.2.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.1.0"
19
+ version = "0.2.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.1.0"
31
+ version = "0.2.0"
32
32
  dependencies = [
33
33
  "serde",
34
34
  ]
35
35
 
36
36
  [[package]]
37
37
  name = "abap-model"
38
- version = "0.1.0"
38
+ version = "0.2.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.1.0"
47
+ version = "0.2.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.1.0"
58
+ version = "0.2.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.1.0"
66
+ version = "0.2.0"
67
67
  dependencies = [
68
68
  "base64 0.22.1",
69
69
  "serde",
@@ -72,7 +72,7 @@ dependencies = [
72
72
 
73
73
  [[package]]
74
74
  name = "abap-sql"
75
- version = "0.1.0"
75
+ version = "0.2.0"
76
76
  dependencies = [
77
77
  "abap-model",
78
78
  "abap-syntax",
@@ -82,7 +82,7 @@ dependencies = [
82
82
 
83
83
  [[package]]
84
84
  name = "abap-syntax"
85
- version = "0.1.0"
85
+ version = "0.2.0"
86
86
  dependencies = [
87
87
  "abap-lexer",
88
88
  "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.1.0"
6
+ version = "0.2.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.1.0
3
+ Version: 0.2.0
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3
@@ -17,7 +17,7 @@ Project-URL: Repository, https://github.com/albertlast/abapPaser
17
17
 
18
18
  # abap-parser
19
19
 
20
- Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API.
20
+ Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end, also through SUBMIT, ABAP memory and files. CDS views tell their data sources and the fields of each element. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API, SAP standard code and dictionary types included.
21
21
 
22
22
  ```sh
23
23
  pip install abap-parser
@@ -45,7 +45,12 @@ options = {
45
45
  }
46
46
  graph = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], options, depth=2)
47
47
  logic = await abap_parser.logic_object_async("PROG:ZREPORT", options)
48
+ # compact text, e.g. for a language model
49
+ prompt = abap_parser.text("logic", source)
50
+ outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
51
+ # a CDS view: sources, joins, associations, the fields of each element
52
+ view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
48
53
  ```
49
54
 
50
- 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`. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
55
+ 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.
51
56
 
@@ -1,6 +1,6 @@
1
1
  # abap-parser
2
2
 
3
- Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API.
3
+ Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end, also through SUBMIT, ABAP memory and files. CDS views tell their data sources and the fields of each element. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API, SAP standard code and dictionary types included.
4
4
 
5
5
  ```sh
6
6
  pip install abap-parser
@@ -28,6 +28,11 @@ options = {
28
28
  }
29
29
  graph = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], options, depth=2)
30
30
  logic = await abap_parser.logic_object_async("PROG:ZREPORT", options)
31
+ # compact text, e.g. for a language model
32
+ prompt = abap_parser.text("logic", source)
33
+ outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
34
+ # a CDS view: sources, joins, associations, the fields of each element
35
+ view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
31
36
  ```
32
37
 
33
- 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`. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
38
+ 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,6 +1,6 @@
1
1
  # abap-parser
2
2
 
3
- Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API.
3
+ Parses ABAP code and tells what it does: classes, methods, forms and declarations; which database tables a statement reads or writes; which methods, function modules and forms it calls; and the logic of each procedure, from conditions and data flow to what it writes in the end, also through SUBMIT, ABAP memory and files. CDS views tell their data sources and the fields of each element. Sources come from a folder (abapGit layout or plain `.abap` files) or from an SAP system through the ADT REST API, SAP standard code and dictionary types included.
4
4
 
5
5
  ```sh
6
6
  pip install abap-parser
@@ -28,6 +28,11 @@ options = {
28
28
  }
29
29
  graph = abap_parser.call_graph(["CLAS:ZCL_ORDERS"], options, depth=2)
30
30
  logic = await abap_parser.logic_object_async("PROG:ZREPORT", options)
31
+ # compact text, e.g. for a language model
32
+ prompt = abap_parser.text("logic", source)
33
+ outline = await abap_parser.object_text_async("symbols", ["CLAS:ZCL_ORDERS"], options)
34
+ # a CDS view: sources, joins, associations, the fields of each element
35
+ view = abap_parser.cds(open("zi_orders.ddls.asddls", encoding="utf-8").read())
31
36
  ```
32
37
 
33
- 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`. See the [repository](https://github.com/albertlast/abapPaser) for what each result contains.
38
+ 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.
@@ -61,6 +61,29 @@ mod _native {
61
61
  py.detach(|| abap_analysis::json::statements(source))
62
62
  }
63
63
 
64
+ #[pyfunction]
65
+ fn cds_json(py: Python<'_>, source: &str) -> String {
66
+ py.detach(|| abap_analysis::json::cds(source))
67
+ }
68
+
69
+ #[pyfunction]
70
+ fn text(py: Python<'_>, analysis: &str, source: &str) -> PyResult<String> {
71
+ py.detach(|| abap_analysis::json::text(analysis, source))
72
+ .map_err(pyo3::exceptions::PyValueError::new_err)
73
+ }
74
+
75
+ #[pyfunction]
76
+ fn object_text(
77
+ py: Python<'_>,
78
+ analysis: &str,
79
+ objects: Vec<String>,
80
+ options: &str,
81
+ depth: u32,
82
+ ) -> PyResult<String> {
83
+ py.detach(|| abap_analysis::json::object_text(analysis, &objects, options, depth))
84
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)
85
+ }
86
+
64
87
  #[pyfunction]
65
88
  fn version() -> &'static str {
66
89
  env!("CARGO_PKG_VERSION")
@@ -102,6 +102,22 @@ class SymbolsTest(unittest.TestCase):
102
102
  db = [e for e in fm["effects"] if e["scope"] == "database"]
103
103
  self.assertTrue(db and all(e["via"] for e in db))
104
104
 
105
+ def test_cds(self):
106
+ d = abap_parser.cds("define view entity zi_c as select from t005 { key land1 as Country }")
107
+ self.assertEqual(d["kind"], "view entity")
108
+ self.assertEqual(d["elements"][0]["fields"], ["T005.LAND1"])
109
+ self.assertIn("key Country = T005.LAND1", abap_parser.text("cds", "define view entity zi_c as select from t005 { key land1 as Country }"))
110
+
111
+ def test_text(self):
112
+ t = abap_parser.text("symbols", SOURCE)
113
+ self.assertTrue(t.startswith('CLASS zcl_demo DEFINITION PUBLIC FINAL. " 2-7\n'), t)
114
+ self.assertIn(' METHODS run IMPORTING iv_id TYPE i RETURNING VALUE(rv_ok) TYPE abap_bool. " 4\n', t)
115
+ self.assertIn(" writes rv_ok (parameter) at 10", abap_parser.text("logic", SOURCE))
116
+ with self.assertRaises(ValueError):
117
+ abap_parser.text("nope", SOURCE)
118
+ with self.assertRaises(RuntimeError):
119
+ abap_parser.object_text("graph", ["CLAS:X"], {})
120
+
105
121
  @unittest.skipUnless((CORPUS / "abapgit" / "src").exists(), "corpus submodule missing")
106
122
  def test_real_class(self):
107
123
  path = CORPUS / "abapgit" / "src" / "apack" / "zcl_abapgit_apack_migration.clas.abap"
@@ -119,6 +135,7 @@ class AsyncTest(unittest.IsolatedAsyncioTestCase):
119
135
  self.assertEqual(await abap_parser.symbols_async(SOURCE), abap_parser.symbols(SOURCE))
120
136
  self.assertEqual(await abap_parser.logic_async(SOURCE), abap_parser.logic(SOURCE))
121
137
  self.assertEqual(await abap_parser.statements_async(SOURCE), abap_parser.statements(SOURCE))
138
+ self.assertEqual(await abap_parser.text_async("logic", SOURCE), abap_parser.text("logic", SOURCE))
122
139
  with self.assertRaises(RuntimeError):
123
140
  await abap_parser.call_graph_async(["CLAS:X"], {})
124
141
 
@@ -8,17 +8,32 @@ use abap_syntax::{File, Statement, StatementKind as K, Token, TokenKind};
8
8
 
9
9
  use crate::context::statement_blocks;
10
10
 
11
- /// Type information about classes and interfaces, possibly outside the file.
11
+ /// The type of a value as far as calls need it.
12
+ #[derive(Debug, Clone, PartialEq, Eq)]
13
+ pub enum ValueType {
14
+ /// `TYPE REF TO name`: a class or interface, or a data reference
15
+ Ref(String),
16
+ /// `TYPE name`: e.g. a structure or a data element, which may be typed
17
+ /// as a reference itself
18
+ Data(String),
19
+ }
20
+
21
+ /// Type information about classes, interfaces and other types, possibly
22
+ /// outside the file.
12
23
  pub trait TypeResolver {
13
- /// Class or interface of the RETURNING parameter, when typed `TYPE REF TO`.
14
- fn returning_class(&self, class: &str, method: &str) -> Option<String>;
15
- /// Class or interface of an attribute typed `TYPE REF TO`.
16
- fn attribute_class(&self, class: &str, attribute: &str) -> Option<String>;
24
+ /// Type of the RETURNING parameter of a method.
25
+ fn returning_type(&self, class: &str, method: &str) -> Option<ValueType>;
26
+ /// Type of an attribute.
27
+ fn attribute_type(&self, class: &str, attribute: &str) -> Option<ValueType>;
17
28
  fn superclass(&self, class: &str) -> Option<String>;
18
- /// Parameters typed `TYPE REF TO` of a method, as (name, class); `None`
19
- /// when the method is unknown. Follows interfaces, aliases and, for
20
- /// redefinitions, the superclass.
21
- fn method_parameters(&self, class: &str, method: &str) -> Option<Vec<(String, String)>>;
29
+ /// Typed parameters of a method; `None` when the method is unknown.
30
+ /// Follows interfaces, aliases and, for redefinitions, the superclass.
31
+ fn method_parameters(&self, class: &str, method: &str) -> Option<Vec<(String, ValueType)>>;
32
+ /// Type of a component of a structure type, local or from the dictionary.
33
+ fn component_type(&self, structure: &str, component: &str) -> Option<ValueType>;
34
+ /// Class or interface a data element or type refers to, when it is
35
+ /// typed as a reference.
36
+ fn data_element_reference(&self, name: &str) -> Option<String>;
22
37
  }
23
38
 
24
39
  /// Resolves within one set of symbols, e.g. one file.
@@ -64,17 +79,47 @@ pub(crate) fn type_info<'a>(symbols: &'a m::FileSymbols, name: &str) -> Option<T
64
79
  })
65
80
  }
66
81
 
67
- fn ref_class(t: &Option<m::Typing>) -> Option<String> {
82
+ fn value_type(t: &Option<m::Typing>) -> Option<ValueType> {
68
83
  match t {
69
- Some(t) if t.kind == m::TypingKind::TypeRefTo => t.name.clone(),
84
+ Some(t) if t.kind == m::TypingKind::TypeRefTo => t.name.clone().map(ValueType::Ref),
85
+ Some(t) if t.kind == m::TypingKind::Type => t.name.clone().map(ValueType::Data),
70
86
  _ => None,
71
87
  }
72
88
  }
73
89
 
90
+ /// A type declared with TYPES anywhere in the symbols.
91
+ fn local_type<'a>(s: &'a m::FileSymbols, name: &str) -> Option<&'a m::Declaration> {
92
+ let is = |d: &&m::Declaration| {
93
+ d.kind == m::DeclarationKind::Type && d.name.eq_ignore_ascii_case(name)
94
+ };
95
+ let classes = s.classes.iter().flat_map(|c| {
96
+ c.types.iter().chain(&c.attributes).chain(
97
+ c.implementation
98
+ .iter()
99
+ .flat_map(|i| i.methods.iter().flat_map(|m| m.locals.iter())),
100
+ )
101
+ });
102
+ s.declarations
103
+ .iter()
104
+ .chain(classes)
105
+ .chain(
106
+ s.interfaces
107
+ .iter()
108
+ .flat_map(|i| i.types.iter().chain(&i.attributes)),
109
+ )
110
+ .chain(
111
+ s.forms
112
+ .iter()
113
+ .chain(&s.function_modules)
114
+ .flat_map(|f| f.locals.iter()),
115
+ )
116
+ .find(is)
117
+ }
118
+
74
119
  impl TypeResolver for SymbolsResolver<'_> {
75
- fn returning_class(&self, class: &str, method: &str) -> Option<String> {
120
+ fn returning_type(&self, class: &str, method: &str) -> Option<ValueType> {
76
121
  let Some(info) = type_info(self.symbols, class) else {
77
- return self.fallback.and_then(|f| f.returning_class(class, method));
122
+ return self.fallback.and_then(|f| f.returning_type(class, method));
78
123
  };
79
124
  let (intf, meth) = match method.split_once('~') {
80
125
  Some((i, mth)) => (Some(i), mth),
@@ -90,7 +135,7 @@ impl TypeResolver for SymbolsResolver<'_> {
90
135
  .parameters
91
136
  .iter()
92
137
  .find(|p| p.direction == m::Direction::Returning)
93
- .and_then(|p| ref_class(&p.typing));
138
+ .and_then(|p| value_type(&p.typing));
94
139
  }
95
140
  if let Some(a) = info
96
141
  .aliases
@@ -98,36 +143,40 @@ impl TypeResolver for SymbolsResolver<'_> {
98
143
  .find(|a| a.name.eq_ignore_ascii_case(meth))
99
144
  {
100
145
  if let Some((i, mth)) = a.component.split_once('~') {
101
- return self.returning_class(i, mth);
146
+ return self.returning_type(i, mth);
102
147
  }
103
148
  }
104
149
  }
105
150
  for i in &info.interfaces {
106
151
  if intf.is_none_or(|x| x.eq_ignore_ascii_case(i)) {
107
- if let Some(c) = self.returning_class(i, meth) {
152
+ if let Some(c) = self.returning_type(i, meth) {
108
153
  return Some(c);
109
154
  }
110
155
  }
111
156
  }
112
157
  let superclass = info.superclass.map(str::to_string);
113
- superclass.and_then(|s| self.returning_class(&s, method))
158
+ superclass.and_then(|s| self.returning_type(&s, method))
114
159
  }
115
160
 
116
- fn attribute_class(&self, class: &str, attribute: &str) -> Option<String> {
161
+ fn attribute_type(&self, class: &str, attribute: &str) -> Option<ValueType> {
117
162
  let Some(info) = type_info(self.symbols, class) else {
163
+ // a reference to a structure: lr->component
164
+ if local_type(self.symbols, class).is_some() {
165
+ return self.component_type(class, attribute);
166
+ }
118
167
  return self
119
168
  .fallback
120
- .and_then(|f| f.attribute_class(class, attribute));
169
+ .and_then(|f| f.attribute_type(class, attribute));
121
170
  };
122
171
  if let Some(a) = info
123
172
  .attributes
124
173
  .iter()
125
174
  .find(|a| a.name.eq_ignore_ascii_case(attribute))
126
175
  {
127
- return ref_class(&a.typing);
176
+ return value_type(&a.typing);
128
177
  }
129
178
  let superclass = info.superclass.map(str::to_string);
130
- superclass.and_then(|s| self.attribute_class(&s, attribute))
179
+ superclass.and_then(|s| self.attribute_type(&s, attribute))
131
180
  }
132
181
 
133
182
  fn superclass(&self, class: &str) -> Option<String> {
@@ -137,7 +186,7 @@ impl TypeResolver for SymbolsResolver<'_> {
137
186
  }
138
187
  }
139
188
 
140
- fn method_parameters(&self, class: &str, method: &str) -> Option<Vec<(String, String)>> {
189
+ fn method_parameters(&self, class: &str, method: &str) -> Option<Vec<(String, ValueType)>> {
141
190
  let Some(info) = type_info(self.symbols, class) else {
142
191
  return self
143
192
  .fallback
@@ -155,7 +204,7 @@ impl TypeResolver for SymbolsResolver<'_> {
155
204
  return Some(
156
205
  md.parameters
157
206
  .iter()
158
- .filter_map(|p| ref_class(&p.typing).map(|c| (p.name.clone(), c)))
207
+ .filter_map(|p| value_type(&p.typing).map(|c| (p.name.clone(), c)))
159
208
  .collect(),
160
209
  );
161
210
  }
@@ -177,6 +226,32 @@ impl TypeResolver for SymbolsResolver<'_> {
177
226
  let superclass = info.superclass.map(str::to_string);
178
227
  superclass.and_then(|sc| self.method_parameters(&sc, method))
179
228
  }
229
+
230
+ fn component_type(&self, structure: &str, component: &str) -> Option<ValueType> {
231
+ match local_type(self.symbols, structure) {
232
+ Some(d) => d
233
+ .components
234
+ .iter()
235
+ .find(|c| c.name.eq_ignore_ascii_case(component))
236
+ .and_then(|c| value_type(&c.typing)),
237
+ None => self
238
+ .fallback
239
+ .and_then(|f| f.component_type(structure, component)),
240
+ }
241
+ }
242
+
243
+ fn data_element_reference(&self, name: &str) -> Option<String> {
244
+ match local_type(self.symbols, name) {
245
+ Some(d) => match value_type(&d.typing)? {
246
+ ValueType::Ref(c) => Some(c),
247
+ ValueType::Data(other) if !other.eq_ignore_ascii_case(name) => {
248
+ self.data_element_reference(&other)
249
+ }
250
+ ValueType::Data(_) => None,
251
+ },
252
+ None => self.fallback.and_then(|f| f.data_element_reference(name)),
253
+ }
254
+ }
180
255
  }
181
256
 
182
257
  /// Built-in functions; a bare call is one unless the class has a method of
@@ -308,9 +383,9 @@ fn is_declaration(kind: K) -> bool {
308
383
  )
309
384
  }
310
385
 
311
- /// Variables with a known class or interface type, per processing block.
386
+ /// Variables with a known type, per processing block.
312
387
  struct Scope {
313
- vars: HashMap<String, String>,
388
+ vars: HashMap<String, ValueType>,
314
389
  class: Option<String>,
315
390
  }
316
391
 
@@ -443,7 +518,7 @@ impl<'a> CallExtractor<'a> {
443
518
 
444
519
  /// Declared variable types visible in a processing block.
445
520
  fn scope(&self, block: Option<&m::BlockRef>, blocks: &[Option<m::BlockRef>]) -> Scope {
446
- let mut vars: HashMap<String, String> = HashMap::new();
521
+ let mut vars: HashMap<String, ValueType> = HashMap::new();
447
522
  add_declarations(&mut vars, &self.symbols.declarations);
448
523
  let mut class = None;
449
524
  if let Some(b) = block {
@@ -521,9 +596,10 @@ impl<'a> CallExtractor<'a> {
521
596
  let declares = t.get(k + 1).is_some_and(|x| x.is("DATA") || x.is("FINAL"));
522
597
  if declares && t.get(k + 2).is_some_and(|x| x.kind == TokenKind::ParenLeft) {
523
598
  if let Some(var) = t.get(k + 3) {
524
- scope
525
- .vars
526
- .insert(var.text.to_ascii_lowercase(), class.text.clone());
599
+ scope.vars.insert(
600
+ var.text.to_ascii_lowercase(),
601
+ ValueType::Ref(class.text.clone()),
602
+ );
527
603
  }
528
604
  }
529
605
  }
@@ -550,11 +626,11 @@ impl<'a> CallExtractor<'a> {
550
626
  let rhs = &t[5..t.len() - 1];
551
627
  let class =
552
628
  if (rhs[0].is("NEW") || rhs[0].is("CAST")) && rhs.len() > 1 && rhs[1].text != "#" {
553
- Some(rhs[1].text.clone())
629
+ Some(ValueType::Ref(rhs[1].text.clone()))
554
630
  } else {
555
- // a call chain: the type of its result
556
- let end = rhs.len();
557
- self.chain_class(rhs, 0, end, &scope)
631
+ // a call chain: the type of its result, resolved when used
632
+ let end = operand_end(rhs, 0).min(rhs.len());
633
+ self.units_type(&units(rhs, 0, end), &scope, None)
558
634
  };
559
635
  if let Some(c) = class {
560
636
  scope.vars.insert(var, c);
@@ -816,6 +892,13 @@ impl<'a> CallExtractor<'a> {
816
892
  );
817
893
  if name != "#" {
818
894
  c.class = Some(name);
895
+ } else if let Some(eq) = start
896
+ .checked_sub(2)
897
+ .filter(|e| *e > 0 && matches!(t[*e].text.as_str(), "=" | "?="))
898
+ {
899
+ // x = NEW #( ): the type of the target
900
+ c.class = self.receiver_class(t, 0, eq, scope);
901
+ c.receiver = Some(join(t, 0, eq, &self.file.lexed.source));
819
902
  }
820
903
  out.push(c);
821
904
  continue;
@@ -933,57 +1016,114 @@ impl<'a> CallExtractor<'a> {
933
1016
  scope: &Scope,
934
1017
  next: Option<TokenKind>,
935
1018
  ) -> Option<String> {
1019
+ self.class_of(self.units_type(units, scope, next)?)
1020
+ }
1021
+
1022
+ /// Type of the value of chain elements; `next` is the arrow after them.
1023
+ fn units_type(
1024
+ &self,
1025
+ units: &[Unit],
1026
+ scope: &Scope,
1027
+ next: Option<TokenKind>,
1028
+ ) -> Option<ValueType> {
936
1029
  let first = units.first()?;
937
- let name = first.name.to_ascii_lowercase();
1030
+ // ls_x-ref: a component of a structure
1031
+ let (head, path) = split_components(&first.name);
1032
+ let name = head.to_ascii_lowercase();
938
1033
  let first_is_class = match units.get(1) {
939
1034
  Some(u) => u.arrow == Some(TokenKind::StaticArrow),
940
1035
  None => next == Some(TokenKind::StaticArrow),
941
1036
  };
942
- let mut current: Option<String> = if first_is_class && !first.is_call {
943
- Some(first.name.clone())
1037
+ let mut current: Option<ValueType> = if first_is_class && !first.is_call {
1038
+ Some(ValueType::Ref(first.name.clone()))
944
1039
  } else if name == "me" {
945
- scope.class.clone()
1040
+ scope.class.clone().map(ValueType::Ref)
946
1041
  } else if name == "super" {
947
1042
  scope
948
1043
  .class
949
1044
  .as_deref()
950
1045
  .and_then(|c| self.resolver.superclass(c))
1046
+ .map(ValueType::Ref)
951
1047
  } else if first.is_call {
952
1048
  scope
953
1049
  .class
954
1050
  .as_deref()
955
- .and_then(|c| self.resolver.returning_class(c, &first.name))
1051
+ .and_then(|c| self.resolver.returning_type(c, head))
956
1052
  } else {
957
1053
  scope.vars.get(&name).cloned().or_else(|| {
958
1054
  scope
959
1055
  .class
960
1056
  .as_deref()
961
- .and_then(|c| self.resolver.attribute_class(c, &first.name))
1057
+ .and_then(|c| self.resolver.attribute_type(c, head))
962
1058
  })
963
1059
  };
1060
+ if let Some(p) = path {
1061
+ current = self.components(current?, p);
1062
+ }
964
1063
  for u in &units[1..] {
965
- let cls = current?;
1064
+ let cls = self.class_of(current?)?;
1065
+ let (head, path) = split_components(&u.name);
966
1066
  current = if u.is_call {
967
- self.resolver.returning_class(&cls, &u.name)
1067
+ self.resolver.returning_type(&cls, head)
968
1068
  } else {
969
- self.resolver.attribute_class(&cls, &u.name)
1069
+ self.resolver.attribute_type(&cls, head)
970
1070
  };
1071
+ if let Some(p) = path {
1072
+ current = self.components(current?, p);
1073
+ }
971
1074
  }
972
1075
  current
973
1076
  }
1077
+
1078
+ /// Class or interface a value of the type refers to.
1079
+ fn class_of(&self, ty: ValueType) -> Option<String> {
1080
+ match ty {
1081
+ ValueType::Ref(c) => Some(c),
1082
+ ValueType::Data(d) => match d.split_once('-') {
1083
+ // TYPE vbak-field
1084
+ Some((s, path)) => self.class_of(self.components(ValueType::Data(s.into()), path)?),
1085
+ None => self.resolver.data_element_reference(&d),
1086
+ },
1087
+ }
1088
+ }
1089
+
1090
+ /// Type of the components `a-b` of a structure.
1091
+ fn components(&self, mut ty: ValueType, path: &str) -> Option<ValueType> {
1092
+ for component in path.split('-') {
1093
+ let ValueType::Data(s) = ty else {
1094
+ return None;
1095
+ };
1096
+ ty = match s.split_once('-') {
1097
+ Some((head, rest)) => {
1098
+ let inner = self.components(ValueType::Data(head.into()), rest)?;
1099
+ self.components(inner, component)?
1100
+ }
1101
+ None => self.resolver.component_type(&s, component)?,
1102
+ };
1103
+ }
1104
+ Some(ty)
1105
+ }
974
1106
  }
975
1107
 
976
- fn add_declarations(vars: &mut HashMap<String, String>, decls: &[m::Declaration]) {
1108
+ /// `ls_x-a-b` into `ls_x` and the components `a-b`.
1109
+ fn split_components(name: &str) -> (&str, Option<&str>) {
1110
+ match name.split_once('-') {
1111
+ Some((head, path)) if !head.is_empty() && !path.is_empty() => (head, Some(path)),
1112
+ _ => (name, None),
1113
+ }
1114
+ }
1115
+
1116
+ fn add_declarations(vars: &mut HashMap<String, ValueType>, decls: &[m::Declaration]) {
977
1117
  for d in decls {
978
- if let Some(c) = ref_class(&d.typing) {
1118
+ if let Some(c) = value_type(&d.typing) {
979
1119
  vars.insert(d.name.to_ascii_lowercase(), c);
980
1120
  }
981
1121
  }
982
1122
  }
983
1123
 
984
- fn params(vars: &mut HashMap<String, String>, parameters: &[m::Parameter]) {
1124
+ fn params(vars: &mut HashMap<String, ValueType>, parameters: &[m::Parameter]) {
985
1125
  for p in parameters {
986
- if let Some(c) = ref_class(&p.typing) {
1126
+ if let Some(c) = value_type(&p.typing) {
987
1127
  vars.insert(p.name.to_ascii_lowercase(), c);
988
1128
  }
989
1129
  }
@@ -1284,6 +1424,7 @@ CLASS lcl_main IMPLEMENTATION.
1284
1424
  PERFORM write_log IN PROGRAM zlog_prog IF FOUND.
1285
1425
  SUBMIT zreport AND RETURN.
1286
1426
  CALL TRANSACTION 'VA01'.
1427
+ lo_item = NEW #( ).
1287
1428
  ENDMETHOD.
1288
1429
  METHOD helper.
1289
1430
  ENDMETHOD.
@@ -1330,6 +1471,7 @@ ENDCLASS.
1330
1471
  "Form write_log",
1331
1472
  "Submit ZREPORT",
1332
1473
  "Transaction VA01",
1474
+ "Constructor lcl_item.constructor resolved",
1333
1475
  ]
1334
1476
  );
1335
1477
  assert_eq!(c[13].options, ["DESTINATION 'NONE'"]);