abap-parser 0.2.0__tar.gz → 0.3.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 (65) hide show
  1. {abap_parser-0.2.0 → abap_parser-0.3.0}/Cargo.lock +10 -9
  2. {abap_parser-0.2.0 → abap_parser-0.3.0}/Cargo.toml +1 -1
  3. {abap_parser-0.2.0 → abap_parser-0.3.0}/PKG-INFO +1 -1
  4. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/calls.rs +56 -0
  5. abap_parser-0.3.0/crates/abap-analysis/src/enhance.rs +305 -0
  6. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/lib.rs +1 -0
  7. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/logic.rs +12 -2
  8. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/text.rs +11 -2
  9. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/workspace.rs +704 -85
  10. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-model/src/lib.rs +4 -0
  11. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/Cargo.toml +1 -0
  12. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/src/adt.rs +377 -24
  13. abap_parser-0.3.0/crates/abap-source/src/badi.rs +218 -0
  14. abap_parser-0.3.0/crates/abap-source/src/cache.rs +268 -0
  15. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/src/fs.rs +141 -2
  16. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/src/lib.rs +157 -0
  17. {abap_parser-0.2.0 → abap_parser-0.3.0}/python/abap_parser/types.py +1 -1
  18. abap_parser-0.2.0/crates/abap-source/src/cache.rs +0 -126
  19. {abap_parser-0.2.0 → abap_parser-0.3.0}/LICENSE +0 -0
  20. {abap_parser-0.2.0 → abap_parser-0.3.0}/README.md +0 -0
  21. {abap_parser-0.2.0 → abap_parser-0.3.0}/bindings/python/Cargo.toml +0 -0
  22. {abap_parser-0.2.0 → abap_parser-0.3.0}/bindings/python/LICENSE +0 -0
  23. {abap_parser-0.2.0 → abap_parser-0.3.0}/bindings/python/README.md +0 -0
  24. {abap_parser-0.2.0 → abap_parser-0.3.0}/bindings/python/src/lib.rs +0 -0
  25. {abap_parser-0.2.0 → abap_parser-0.3.0}/bindings/python/tests/test_symbols.py +0 -0
  26. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/Cargo.toml +0 -0
  27. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/args.rs +0 -0
  28. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/context.rs +0 -0
  29. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/db.rs +0 -0
  30. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/ddic.rs +0 -0
  31. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/stores.rs +0 -0
  32. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/src/symbols.rs +0 -0
  33. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/adt_mock.rs +0 -0
  34. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/amdp_corpus.rs +0 -0
  35. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/call_graph_corpus.rs +0 -0
  36. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/corpus_smoke.rs +0 -0
  37. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/logic_corpus.rs +0 -0
  38. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-analysis/tests/symbols_diff.rs +0 -0
  39. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-lexer/Cargo.toml +0 -0
  40. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-lexer/src/lib.rs +0 -0
  41. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-model/Cargo.toml +0 -0
  42. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/src/mock.rs +0 -0
  43. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-source/src/options.rs +0 -0
  44. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/Cargo.toml +0 -0
  45. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/src/cds.rs +0 -0
  46. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/src/hana.rs +0 -0
  47. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/src/lib.rs +0 -0
  48. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/src/tests.rs +0 -0
  49. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/tests/cds_corpus.rs +0 -0
  50. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-sql/tests/sql_diff.rs +0 -0
  51. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/Cargo.toml +0 -0
  52. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/classify.rs +0 -0
  53. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/decl.rs +0 -0
  54. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/expr.rs +0 -0
  55. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/kind.rs +0 -0
  56. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/lib.rs +0 -0
  57. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/operands.rs +0 -0
  58. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/src/structure.rs +0 -0
  59. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/tests/abaplint_statements.rs +0 -0
  60. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/tests/common/mod.rs +0 -0
  61. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/tests/oracle_diff.rs +0 -0
  62. {abap_parser-0.2.0 → abap_parser-0.3.0}/crates/abap-syntax/tests/refs_diff.rs +0 -0
  63. {abap_parser-0.2.0 → abap_parser-0.3.0}/pyproject.toml +0 -0
  64. {abap_parser-0.2.0 → abap_parser-0.3.0}/python/abap_parser/__init__.py +0 -0
  65. {abap_parser-0.2.0 → abap_parser-0.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.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.3.0
4
4
  Classifier: License :: OSI Approved :: MIT License
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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);
@@ -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
+ }
@@ -5,6 +5,7 @@ mod calls;
5
5
  mod context;
6
6
  mod db;
7
7
  mod ddic;
8
+ mod enhance;
8
9
  mod logic;
9
10
  mod stores;
10
11
  mod symbols;
@@ -40,6 +40,7 @@ pub fn logic_of(file: &File) -> m::FileLogic {
40
40
  file,
41
41
  symbols: &symbols,
42
42
  resolver: &resolver,
43
+ program: crate::calls::main_program(&symbols),
43
44
  }
44
45
  .calls();
45
46
  let attributes = |class: &str| class_attributes(&symbols, None, class);
@@ -73,6 +74,8 @@ impl Workspace {
73
74
  /// in the TOP include, has the scope `global`. Calls into other objects
74
75
  /// are followed up to `depth` hops, calls within the object always.
75
76
  pub fn logic(&self, root: &SourceRef, depth: u32) -> m::FileLogic {
77
+ // the engine loads callees one by one: load them before, several at once
78
+ self.preload(std::slice::from_ref(root), depth);
76
79
  let mut engine = Engine::new(Some(self), depth);
77
80
  let procedures = match engine.load(root, 0) {
78
81
  Some(o) => engine.finish(o),
@@ -92,7 +95,7 @@ impl Workspace {
92
95
 
93
96
  /// Calls through an interface are followed into at most this many
94
97
  /// implementations; with more, which one runs says too little.
95
- const MAX_IMPLEMENTATIONS: usize = 10;
98
+ pub(crate) const MAX_IMPLEMENTATIONS: usize = 10;
96
99
 
97
100
  /// Subclasses looked at for redefinitions of a called method; with more,
98
101
  /// a class is too general to tell which redefinition runs.
@@ -463,7 +466,14 @@ impl<'w> Engine<'w> {
463
466
  if !interface || name.contains('~') {
464
467
  return Vec::new();
465
468
  }
466
- let classes = self.implementations(o, &class);
469
+ // CALL BADI runs the active implementations of the BAdI
470
+ let classes = match (&c.badi, self.workspace) {
471
+ (Some(badi), Some(ws)) => ws
472
+ .badi(badi)
473
+ .map(|b| crate::workspace::badi_classes(&b))
474
+ .unwrap_or_default(),
475
+ _ => self.implementations(o, &class),
476
+ };
467
477
  if classes.len() > MAX_IMPLEMENTATIONS {
468
478
  return Vec::new();
469
479
  }
@@ -523,14 +523,23 @@ fn call(c: &m::Call) -> String {
523
523
  if !c.resolved {
524
524
  flags.push("unresolved");
525
525
  }
526
+ // CALL BADI with the BAdI, its interface is the class
527
+ let options: Vec<String> = c
528
+ .options
529
+ .iter()
530
+ .map(|o| match &c.badi {
531
+ Some(b) if o == "BADI" => format!("BADI {b}"),
532
+ _ => o.clone(),
533
+ })
534
+ .collect();
526
535
  format!(
527
536
  " {:>5} {from}: {} {target}{}{}\n",
528
537
  c.span.start.row,
529
538
  format!("{:?}", c.kind).to_ascii_lowercase(),
530
- if c.options.is_empty() {
539
+ if options.is_empty() {
531
540
  String::new()
532
541
  } else {
533
- format!(" [{}]", c.options.join(", "))
542
+ format!(" [{}]", options.join(", "))
534
543
  },
535
544
  if flags.is_empty() {
536
545
  String::new()