| 1915 | } |
| 1916 | |
| 1917 | static int parse_narrowing_one(PHPLSPContext *ctx, TSNode cond, php_narrowing_t *out) { |
| 1918 | if (ts_node_is_null(cond)) |
| 1919 | return 0; |
| 1920 | const char *kind = ts_node_type(cond); |
| 1921 | if (strcmp(kind, "parenthesized_expression") == 0) { |
| 1922 | uint32_t nc = ts_node_child_count(cond); |
| 1923 | for (uint32_t i = 0; i < nc; i++) { |
| 1924 | TSNode c = ts_node_child(cond, i); |
| 1925 | if (!ts_node_is_null(c) && ts_node_is_named(c)) { |
| 1926 | return parse_narrowing_one(ctx, c, out); |
| 1927 | } |
| 1928 | } |
| 1929 | return 0; |
| 1930 | } |
| 1931 | |
| 1932 | /* `$x instanceof Foo` — emitted as binary_expression with operator |
| 1933 | * "instanceof" in tree-sitter-php. */ |
| 1934 | if (strcmp(kind, "binary_expression") == 0) { |
| 1935 | TSNode left = ts_node_child_by_field_name(cond, "left", 4); |
| 1936 | TSNode op = ts_node_child_by_field_name(cond, "operator", 8); |
| 1937 | TSNode right = ts_node_child_by_field_name(cond, "right", 5); |
| 1938 | if (ts_node_is_null(left) || ts_node_is_null(right)) |
| 1939 | return false; |
| 1940 | char *opt = !ts_node_is_null(op) ? php_node_text(ctx, op) : NULL; |
| 1941 | if (opt && strcmp(opt, "instanceof") == 0) { |
| 1942 | if (!node_is(left, "variable_name")) |
| 1943 | return false; |
| 1944 | char *vt = php_node_text(ctx, left); |
| 1945 | if (!vt) |
| 1946 | return false; |
| 1947 | const char *vname = (vt[0] == '$') ? vt + 1 : vt; |
| 1948 | char *rt = php_node_text(ctx, right); |
| 1949 | if (!rt) |
| 1950 | return false; |
| 1951 | const char *resolved = php_resolve_class_name(ctx, rt); |
| 1952 | if (!resolved) |
| 1953 | return false; |
| 1954 | out->var_name = cbm_arena_strdup(ctx->arena, vname); |
| 1955 | out->type = cbm_type_named(ctx->arena, resolved); |
| 1956 | return true; |
| 1957 | } |
| 1958 | /* `$x !== null` / `$x != null` / `null !== $x` — narrow to "non-null": |
| 1959 | * we keep the existing scope type, so this is a no-op for type |
| 1960 | * purposes but suppresses null branches downstream. We don't |
| 1961 | * subtract nullable so just return false here. */ |
| 1962 | } |
| 1963 | |
| 1964 | /* `is_string($x)` / `is_int($x)` / `array_key_exists(...)` / |
| 1965 | * `method_exists($x, 'foo')` / `is_a($x, Foo::class)` / ... */ |
| 1966 | if (strcmp(kind, "function_call_expression") == 0) { |
| 1967 | TSNode fn = ts_node_child_by_field_name(cond, "function", 8); |
| 1968 | TSNode args = ts_node_child_by_field_name(cond, "arguments", 9); |
| 1969 | if (ts_node_is_null(fn)) |
| 1970 | return false; |
| 1971 | char *name = php_node_text(ctx, fn); |
| 1972 | if (!name) |
| 1973 | return false; |
| 1974 | /* `is_a($x, Foo::class)` and `is_a($x, 'Foo')` narrow $x to Foo. */ |
no test coverage detected