Automated labelling is a really hard problem, but feature geometry is not so bad.
Even if you can get placement to work adequately most of the time, there are likely to be exceptions. Some of these you will notice and may be able to address. Others you won't notice when making a large map or tileset because you can't pour over every inch of your map at a variety of scales. Almost always you will have the urge to move some automatically-placed labels manually, from a cartographic perspective.
As I suggested in my comment, I'd make the problem easier for the labelling engine. In this case, I would do this by defining my rivers as a table view*, with river geometries clipped to respect lake boundaries. That way, there are no river features inside lakes to be labelled, and no label collisions.
* I assume the use of a RDBMS here, like PostgreSQL/PostGIS, for convenience and the ability to only update your authoritative source of data and have the view work itself out without your intervention. But you can also do some work upfront with static files to clip and delete features, but I don't recommend this if you ever plan to revisit a map.
Starting with two shapefiles (could be database tables) of rivers and lakes, with rivers intersecting lakes and causing labelling issues that are hard to resolve completely and confidently:
Bring these into Postgres if you need to with shp2pgsql:
shp2pgsql -s 4326 /data/lake public.lakes | psql -d mydb
shp2pgsql -s 4326 /data/river public.rivers | psql -d mydb
Then define a view with ST_Difference:
CREATE OR REPLACE VIEW rivers_clipped AS
SELECT r.id, ST_Difference(r.geom, l.geom) AS geom, r.name
FROM public.rivers AS r, public.lakes AS l;
Add the view to your layout:
Although the problem in my example is deliberately fabricated, the styles in the two river layers (original and view) are the same, and they are placed on top of the lake in the drawing order. When you update the lakes or rivers geometries, you won't need to do much more than refresh the rendering.