fix(intellisense): access labels apply to statements that open a nested brace

_scan_text applied `public:` / `private:` / `protected:` only inside
handle_statement, which the `{` branch skipped whenever the statement opened
a nested enum, struct or class. So `public:\n typedef enum { ... }` -- the
first thing in AccelStepper's class body, and a common idiom -- took the enum
branch, the label was never seen, the class stayed in its default private
access, and every method after the enum was dropped. AccelStepper shipped 4
of its public methods; DallasTemperature, SoftWire, MFRC522_I2C, RTCZero
shipped none.

Labels are now consumed before the branch. Rescanned all 1,125 entries of
the production library cache: 82 libraries gain members (+1,532 total,
DallasTemperature 0 -> 57), and the 20 that "lose" one or two were leaking
private/protected members past the old check (PMS::loop is under private:,
hd44780::waitReady under protected:) -- precision, not regression.
This commit is contained in:
David Montero 2026-08-17 18:47:46 +02:00
parent 8f412ef8bf
commit 15126cd27a
2 changed files with 54 additions and 4 deletions

View File

@ -368,23 +368,38 @@ def _scan_text(text: str, add: Callable[[dict], Optional[dict]], detail: str) ->
stack: list[dict] = []
i, n = 0, len(text)
def handle_statement(stmt: str) -> None:
def apply_access(stmt: str) -> str:
"""Consume `public:` / `private:` / `protected:` labels at the head of
a statement, updating the enclosing class scope, and return the rest.
Must run for EVERY statement inside a class, including the ones that
open a nested enum/struct/brace: `public:\n typedef enum {` used to
take the enum branch without ever seeing its `public:`, leaving the
class in its default private access and dropping every method that
followed (AccelStepper lost its whole API that way)."""
if not stack or stack[-1]["kind"] != "classlike":
return
return stmt
scope = stack[-1]
candidate = stmt
for m in _ACCESS_RE.finditer(stmt):
scope["access"] = m.group(1)
candidate = stmt[m.end():]
if scope["access"] != "public":
return candidate
def handle_statement(stmt: str) -> None:
candidate = apply_access(stmt)
if not stack or stack[-1]["kind"] != "classlike":
return
_try_method(candidate.strip(), scope, add, detail)
if stack[-1]["access"] != "public":
return
_try_method(candidate.strip(), stack[-1], add, detail)
while i < n:
c = text[i]
if c == "{":
stmt = "".join(buf).strip()
buf = []
# Access labels apply whichever branch the statement takes below.
stmt = apply_access(stmt)
if "template" not in stmt:
em = _ENUM_RE.search(stmt)
if em and re.search(r"\benum\b", stmt):

View File

@ -123,6 +123,41 @@ def test_scan_library_constants_and_enums(fake_lib: Path) -> None:
# ── spec parsing / normalization (used by the route) ──────────────────────
def test_access_label_survives_a_nested_enum(tmp_path: Path) -> None:
"""`public:` followed by a nested typedef enum used to take the enum branch
of the walker without ever applying the access label, so the class stayed
private and every method after the enum vanished. AccelStepper shipped 4
of its ~50 public methods that way; DallasTemperature shipped 0."""
lib = tmp_path / "steplib@1.0.0-abcdefabcdef"
src = lib / "src"
src.mkdir(parents=True)
(lib / "library.properties").write_text("name=Step Lib\nversion=1.0.0\n", encoding="utf-8")
(src / "Step.h").write_text(
"""\
class Stepper {
public:
typedef enum { FUNCTION = 0, DRIVER = 1 } MotorInterfaceType;
Stepper(uint8_t iface = DRIVER);
void moveTo(long absolute);
bool run();
protected:
void step(long n);
private:
int _pin;
public:
void setMaxSpeed(float speed);
};
""",
encoding="utf-8",
)
result = scan_library(lib)
methods = _by_kind(result, "method")
assert set(methods) == {"moveTo", "run", "setMaxSpeed"}
assert all(m["owner"] == "Stepper" for m in methods.values())
assert "step" not in methods and "_pin" not in methods
assert {"FUNCTION", "DRIVER"} <= set(_by_kind(result, "constant"))
def test_parse_spec() -> None:
assert parse_spec("Adafruit GFX Library@1.12.6") == ("Adafruit GFX Library", "1.12.6")
assert parse_spec("FastLED") == ("FastLED", None)